Reputation: 42235
#include <unordered_map>
#include <type_traits>
int main()
{
std::unordered_multimap<int, string> m{ { 1, "hello" } };
auto b = std::is_move_assignable_v<decltype(*m.begin())>;
// b is true
auto v = *m4.begin(); // ok
v = std::move(*m4.begin()); // compile-time error
}
Issue:
If
b
is true, thenv = *m4.begin();
should be ok.
Question:
Why does
std::is_move_assignable
not behave as expected?
Error messages: (Clang 3.8 + Visual Studio 2015 Update 3)
error : cannot assign to non-static data member 'first' with const-qualified type 'const int' first = _Right.first; ~~~~~ ^ main.cpp(119,5) : note: in instantiation of member function 'std::pair<const int, std::basic_string<char, std::char_traits<char>, std::allocator<char> > >::operator=' requested here v = *m4.begin(); // error
Upvotes: 2
Views: 987
Reputation:
If
b
is true, thenv = *m4.begin();
should be ok.
v = *m4.begin();
is copy assignment, not move assignment. Move assignment is v = std::move(*m4.begin());
.
But as T.C. points out and Yam Marcovic answered as well, your use of std::is_move_assignable
is wrong, because decltype(*m.begin())
is a reference type. You end up not quite checking for move assignability, and your check does end up right for v = *m4.begin();
.
For move assignment, the check should be std::is_move_assignable_v<std::remove_reference_t<decltype(*m.begin())>>
.
Anyway, is_move_assignable
doesn't try too hard to check whether the type is move assignable, it only checks whether the type reports itself as move assignable.
Given
template <typename T>
struct S {
S &operator=(S &&) { T() = "Hello"; return *this; }
};
std::is_move_assignable<S<int>>::value
will report true
, because the signature S<int> S::operator=(S<int> &&);
is well-formed. Attempting to actually call it will trigger an error, since T()
cannot be assigned to, and even if it could be, it couldn't be assigned a string.
In your case, the type is std::pair<const int, std::string>
. Note the const
there.
std::pair<T1, T2>
currently always declares operator=
, but if either T1
or T2
cannot be assigned to, attempting to actually use it will produce an error.
Class templates may make sure that a valid signature of operator=
is only declared if its instantiation would be well-formed to avoid this problem, and as also pointed out by T.C., std::pair
will do so in the future.
Upvotes: 6
Reputation: 8141
First, as *begin()
returns an lvalue reference, you're actually passing an lvalue reference to std::is_move_assignable
, which is then implemented by means of std::is_assignable<T&, T&&>
. Now, notice the forwarding reference there, which converts to an lvalue reference. This means that by asking std::is_move_assignable<SomeType&>
you have effectively asked std::is_assignable<T&, T&>
—i.e. copy-assignable. Granted, this is a bit misleading.
You might want to test this code to see my point:
#include <iostream>
#include <type_traits>
struct S {
S& operator=(const S&) = default;
S& operator=(S&&) = delete;
};
int main()
{
std::cout << std::is_move_assignable<S>::value; // 0
std::cout << std::is_move_assignable<S&>::value; // 1
}
All that said, *begin()
in this case returns a pair<const int, string>&
, so it would not be any-kind-of-assignable anyhow, because you cannot assign to a const int
. However, as @hvd pointed out, since the function is declared in principal in the pair
template, the type trait does not understand that if it were ever generated into an actual function by the compiler, the latter would encounter an error.
Upvotes: 3
Reputation: 1184
Dereferencing .begin()
iterator of empty container is undefined behaviour.
Upvotes: 0