Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding `std::is_move_constructible`

Types without a move constructor, but with a copy constructor that accepts const T& arguments, satisfy std::is_move_constructible. For example, in the following code:

#include <type_traits>

struct T {
    T(const T&) {}
    //T(T&&) = delete;
};

int main() {
    static_assert(std::is_move_constructible<T>::value, "not move constructible");
    return 0;
}

T will have no implicit move constructor as it has a user-defined copy constructor.

However, if we uncomment the explicit delete of the move constructor, the code no longer compiles. Why is this? I would have expected that the explicit copy constructor would still satisfy std::is_move_constructible.

Does overload play a role, choosing the declared move constructor and then failing because it is deleted?


If this difference between move constructibility between a no implicit move ctor and a deleted move ctor class is mandated by the standard, please quote, and if possible, give a rationale (like "to provide a facility for forbidding move construction"—first thing that jumps to mind).

like image 708
Irfy Avatar asked Nov 26 '15 13:11

Irfy


2 Answers

This is a complete revamp of my first answer, to correct some mistakes said and to have quotes from the standard and to nail some details the questioner wishes.

What std::is_move_constructible actually do

If T is a structure then std::is_move_constructible<T> evaluates to std::is_constructible<T,T&&>. std::is_constructible<T,U> is valid if T x(y) is a well-formed expression for some y of type U. Thus for std::is_move_constructible<T> to be true, T x(std::move(y)) must be well-formed for y of type T.

Quotes from the standard:

The predicate condition for a template specialization is_constructible<T, Args...>
shall be satisfied if and only if the following variable definition would
be well-formed for some invented variable t:
    T t(create<Args>()...);

(...)

Template: template <class T> struct is_move_constructible;
Condition: For a referenceable type T, the same result as is_constructible<T, T&&>::value,
           otherwise false.
Precondition: T shall be a complete type, (possibly cv-qualified) void,
              or an array of unknown bound.

When a move constructor is created

The standard says a default move constructor is created only when no copy constructor, move constructor, assignment operator or a destructor has been declared by the user.

If the definition of a class X does not explicitly declare a move
constructor, one will be implicitly declared as defaulted if and only if
—X does not have a user-declared copy constructor,
—X does not have a user-declared copy assignment operator,
—X does not have a user-declared move assignment operator, and
—X does not have a user-declared destructor

However the standard allows you to initialize a class lvalue-reference with a class rvalue.

Otherwise, the reference shall be an lvalue reference to a non-volatile const type
(i.e., cv1 shall be const), or the reference shall be an rvalue reference.
—If the initializer expression is an xvalue (but not a bit-field),
class prvalue, array prvalue or function lvalue and “cv1 T1”
is reference-compatible with “cv2 T2”, or (...)
then the reference is bound to the value of the initializer expression (...)
(or, in either case, to an appropriate base class subobject).

Thus if you have a copy constructor T::T(S& other) and an object y of type T&&, i.e. a rvalue reference to T, then y is reference-compatible with T& and T x(y) is a valid expression to call the copy constructor T::T(S&).

What the example struct do

Let me go with your first example, removing the const keywords, to avoid stating ten times that the reference needs to be more cv-qualified than the initializer.

struct S {
    S(S&) {}
};

Let's check the condition. There is no implicitly defaulted move constructor since there is a user-defined copy constructor. However, if y is of type S, then std::move(y), of type S&&, is reference compatible with type S&. Thus S x(std::move(y)) is perfectly valid and call the copy constructor S::S(const S&).

What the second example do

struct T {
    T(T&) {}
    T(T&&) = delete;
};

Again, no move constructor is defined as there is a user defined copy constructor and a user-defined move constructor. Again let y be of type T and consider T x(std::move(y)).

However, this time multiple constructor can fit in the expression, so an overload selection is performed. Only the most specialized matching constructor is attempted to work with so only the move constructor T::T(T&&) is attempted to call to. But the move constructor is deleted, so this is invalid.

Conclusion

The first struct, S, can use its copy constructor usable to perform a move-like expression, as it is the most specialized constructor for this expression.

The second struct, T, have to use its explicitly declared move constructor to perform the move-like expression, again because it is the most specialized. However that constructor is deleted, the move-construction expression fails.

like image 58
Lærne Avatar answered Oct 27 '22 06:10

Lærne


Overview

A move constructible class is a class that either has a move constructor, either implicit or user declared. Or a copy constructor that is called for rvalue references. These constructors would be evoked, unless the class has a deleted move constructor.

Consequently:

struct T {
    T(const T&) {}
};

Is a move constructible class and thus you get true from the respective trait.

Declaring though the move constructor delete:

struct T {
    T(const T&) {}
    T(T&&) = delete;
};

Makes your class non move constructible.

Why this happens?

Even though the move constructor is delete, it participates in the overload resolution as if it wasn't delete. Thus, as a direct match is prefered.

For is_move_constructible to provide the member constant value true the variable definition T obj(T&&); must be well-formed, if you delete the move constructor of T is ill-formed. Thus, you get value equal to false.

Now as for how this can happen in compile time. Look for the magic of std::declval. std::declval is used somewhere is the implementation of the trait to evaluate if a move of the class object is possible with out actually evaluating the expression.

like image 32
101010 Avatar answered Oct 27 '22 05:10

101010