Why does [std::is_move_assignable] not behave as expected?

916 views Asked by At
#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, then v = *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
3

There are 3 answers

7
paweldac On

Dereferencing .begin() iterator of empty container is undefined behaviour.

1
Yam Marcovic On

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.

7
AudioBubble On

If b is true, then v = *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.