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 satis?ed if and only if the following variable de?nition 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-quali?ed) 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-?eld),
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.