Li Chen
Li Chen

Reputation: 5260

Why does move constructor affect is_assignable?

Just came from is_assignable and std::unique_ptr. @Angew tells me that because std::unique_ptr<int, do_nothing> and std::unique_ptr<int> are different types, so static_assert(not std::is_assignable<std::unique_ptr<int>, std::unique_ptr<int, do_nothing>>::value, "");. So, I tried:

template<typename T, typename D>
struct MoveAssignOnly_V2
{
    MoveAssignOnly_V2&
    operator=(MoveAssignOnly_V2&)
        = delete;

    MoveAssignOnly_V2&
    operator=(MoveAssignOnly_V2&&) noexcept
    {}
};

int main()
{
      static_assert(not std::is_assignable_v<MoveAssignOnly_V2<int, float>,
                 MoveAssignOnly_V2<int, double>>);
}

Yes, because MoveAssignOnly_V2<int, float> and MoveAssignOnly_V2<int, double> are two different types, so they are not assignable.

But, when I add a a move ctor:

template<class U, class E>
MoveAssignOnly_V2(MoveAssignOnly_V2<U, E>&& m) noexcept {}

static_assert fail!(both gcc and clang).

Question here: Why does move constructor affects is_assignable?

Updated

The reason why I add this constructor is that I found std::unique_ptr have a

template< class U, class E >
unique_ptr( unique_ptr<U, E>&& u ) noexcept;

, which confuses me a little: how can it be not assignable now that it has such a ctor? so I tried add such ctor to MoveAssignOnly_V2 and post this question. The two answers are fine, however, still cannot explain why std::unique_ptr is not assignable when it has both move assignment and this templated constructor.

Upvotes: 3

Views: 122

Answers (2)

Take this code:

MoveAssignOnly_V2<int, float> lhs;
MoveAssignOnly_V2<int, double> rhs;
lhs = stdL::move(rhs);

When the converting constructor (note that it's not a move constructor) is not there, there is no way to assign rhs into lhs.

However, when you add the constructor template, there is now a way to convert rhs into the type MoveAssignOnly_V2<int, float> (which creates a temporary of that type). Then, it's possible to move-assign from that temporary into lhs.

This is the same principle as:

double lhs = 3.14;
float rhs = 42.f;
lhs = std::move(rhs);

To address the update in the question:

You cannot go with function declarations alone, you have to read the full specification (in the standard or suitable reference). Quoting the linked reference about the converting constructor of std::unique_ptr:

This constructor only participates in overload resolution if all of the following is true:

a) unique_ptr<U, E>::pointer is implicitly convertible to pointer
b) U is not an array type
c) Either Deleter is a reference type and E is the same type as D, or Deleter is not a reference type and E is implicitly convertible to D

So as you can see, the unique_ptr's converting constructor must be implemented so that it's only active if the source deleter can be converted to the target one. That's basically the same rule as for move assignment in the previous question.

Upvotes: 4

llllllllll
llllllllll

Reputation: 16404

What you added here:

template<class U, class E>
MoveAssignOnly_V2(MoveAssignOnly_V2<U, E>&& m) noexcept {}

is not only a move constructor, but a templated constructor that can construct MoveAssignOnly_V2<T, D> from any MoveAssignOnly_V2<U, E>.

Thus constructing MoveAssignOnly_V2<int, float> from MoveAssignOnly_V2<int, double>> is fine.

Upvotes: 2

Related Questions