Though the standard deems S(const S&&)
to be "a move constructor" and you may think that would be the end of it, is_move_constructible
actually requires that your declaration of s3
is valid in C++14 and earlier; i.e. that a move constructor will be chosen for that declaration.
From cppref:
If T
is not a referenceable type (i.e., possibly cv-qualified void
or a function type with a cv-qualifier-seq or a ref-qualifier), provides a member constant value equal to false
. Otherwise, provides a member constant value equal to std::is_constructible<T, T&&>::value
.
The problem you have is that S(S&&)
can still be found by overload resolution. This occurs, with S(S&&)
being a better match than S(const S&&)
, before S(S&&)
is found to be deleted.
s3
's declaration is valid in C++17 only because no copies or moves come into it at all. What used to be the "declaration" of a temporary is now just a fancy list of constructor arguments; a matching temporary, if and only if one is needed at all, is "materialised" further down the call stack. This is known as "guaranteed elision", even though no temporary really gets elided at all as it simply doesn't exist in the first place.
Remember that move semantics are an illusion; all you're really doing is having the language "call a function" with the appropriate parameter(s) for the expression you give it as argument(s). s2
works because you pass an rvalue const S
(the value category and type of an expression formed by providing a const S&&
), which can bind to a const S&&
(but not to a S&&
!); whether that counts as "move-constructing just fine" is a matter of perspective.
tl;dr: That trait requires S(S&&)
to work, and you specifically defined it not to.