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).
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With