3

This following code move-constructs S via const &&.
Yet it returns 0, indicating S is not move-constructible!

  1. What is the correct behavior of each of the 2 marked constructions in main according to the standard?

  2. If returning 0 is the correct behavior, what is the rationale behind it?
    (Why shouldn't it reflect whether the type is, in fact, constructible via a move?)

#include <algorithm>

struct S
{
    S(          ) { }
    S(S        &) { }  // = delete; doesn't make any difference
    S(S const  &) { }  // = delete; doesn't make any difference
    S(S const &&) { }
    S(S       &&) = delete;
};

int main()
{
    S const s1;
    S s2(std::move(s1));  // OK for >= C++11
    S s3((S()));          // OK for >= C++17, error for <= C++14
    static_assert(std::is_move_constructible<S>::value);
}
Asteroids With Wings
  • 17,071
  • 2
  • 21
  • 35
user541686
  • 205,094
  • 128
  • 528
  • 886

1 Answers1

9

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.

Asteroids With Wings
  • 17,071
  • 2
  • 21
  • 35
  • Thanks! To clarify some things for anyone reading: (1) If you declare `S(S const &)` but don't declare `S(S &&)`, then does the latter is considered undeclared (*not* deleted). (2) The idea seems to be that construction from a non-const rvalue reference should be possible. Presumably they could've introduced `std::is_const_move_constructible` (and ditto with `volatile`) to complete the picture, but didn't see practical value in it? P.S. I like how you linked to my own question in your answer ;) – user541686 Dec 15 '20 at 22:11
  • @user541686 Such a trait (`std::is_const_move_constructible`) would be used for what? You can't actually take resources from a `const` thing, so a move constructor that looks like `S(const S&&)` is beyond pointless. Frankly I imagine the standard only calls it "a move constructor" to keep the auto-generated-special-member-functions rules simple and symmetrical, but I'm not surprised that `is_move_constructible` doesn't eat it up. No `S(const S&&)` is getting past code review in my projects, that's for sure. – Asteroids With Wings Dec 15 '20 at 22:23
  • And, yes, it is ironic that the problem is fully explored in a previous question that _you_ posed. – Asteroids With Wings Dec 15 '20 at 22:24
  • You can totally take resources from `S const &&`—both from its `mutable` fields and also from anything non-const it points to with unique ownership, for instance. There's nothing wrong with this semantically. This is in contrast with `S const &`, which they *do* consider to be move-constructible, and from which you *could* take resources from in exactly the same situations, but which you should absolutely *not* attempt to do so because it's an l-value and you're semantically supposed to be copying rather than moving. So I can't fathom why it makes sense to allow `const &` but not `const &&`. – user541686 Dec 15 '20 at 22:32
  • @user541686 If you're using `mutable` in such a way, your class is broken. It's supposed to be for things like caches, not actual first-class resources. And if you're "moving" cache state and no actual data, using a `S(const S&&)` then... sorry, I don't really know what the purpose of your program is. – Asteroids With Wings Dec 15 '20 at 22:39
  • The thing about `is_move_constructible` accepting `S(const S&)` is a bit weird but comes down to how the library's requirements are structured; "move constructible" is a superset of "copy constructible" and actually being able to do proper movey things (i.e. it _really_ means "move or copy constructible" but we abstract away which one can/will actually happen, since the semantics should be identical anyway from the perspective of the target). It doesn't mean the language designers intend for you to stick loads of `mutable` things in your class then move them all from it when you do a copy. – Asteroids With Wings Dec 15 '20 at 22:42
  • You're too hung up on my `mutable` example and missing the larger point. Look at the other one. There's literally nothing wrong with moving the target of a `unique_ptr` that's embedded in a `const` instance of your class. It's not even within your control whether the instance is `const`. But my point isn't that you should do this everywhere. My point is (a) it's semantically sound, (b) it's dumb for the language to let us define `const &&` but not utilize it, and (c) it simply doesn't make sense to treat `const &&` as somehow being *less* move-capable than `const &`, no matter how you dice it. – user541686 Dec 16 '20 at 01:31
  • @user541686 _"it simply doesn't make sense to treat `const &&` as somehow being less move-capable than `const &`"_ But, nobody did that. One is precisely as "move-capable" as the other. The only problem is that _you_ defined a better-matching `S(S&&)` then deleted it. That's it. Nothing to do with what the other constructors do; they aren't invoked. – Asteroids With Wings Dec 16 '20 at 13:05
  • I was referring to how `is_move_constructible` yields `false` when `&&` is deleted and `const &&` is declared, and nevertheless yields `true` when only `const &` is declared. As I tried to explain above, moving from `const &&` is semantically sound (even when `&&` is deleted), but moving from `const &` is just wrong, period. Yet the trait is claiming the latter is move-constructible and the former isn't, which is the exact opposite of reality. This is just bonkers to me no matter how you dice it. – user541686 Dec 16 '20 at 13:58
  • Well, I've already explained what's going on, and it's not really "bonkers". You literally wrote code that says "if someone tries to construct this class from a non-const rvalue, the program should not compile". That's what `delete` does, and that's what `is_move_constructible` is telling you that you've done. There's not much point rationalising about the purpose of traits or constructors beyond that. – Asteroids With Wings Dec 16 '20 at 14:14
  • You know what? I figured it out. **It turns out `is_move_constructible` is a thing.** *That's* why `is_move_constructible` ignores the `T const &&` constructor—detecting that isn't impossible; it's merely the job of `is_move_constructible`! And in fact `is_move_constructible` ignores the `T&&` constructor just the same. (And putting it all together, it seems `is_move_constructible` is actually asking, *"Can I move-construct `remove_cv::type` **from `T&&`**?"* which is a little bit more reasonable.) – user541686 Dec 18 '20 at 12:00
  • Yes, you can write `is_move_constructible`, but that's not really the intended use. As I've explained, the way the trait works, is that _"Types without a move constructor, but with a copy constructor that accepts const T& arguments, satisfy std::is_move_constructible."_ (per cppref). That's just a fundamental. – Asteroids With Wings Dec 18 '20 at 13:48
  • I mean, the C++17 standard literally defines it as `is_constructible_v`. They wrote this explicitly bearing in mind the fact that it could be cv-qualified. (*"T shall be a complete type, cv `void`, or an array of unknown bound."*) Seems to me they intended it to work for `const &&`, `volatile &&`, and `const volatile &&` this way. – user541686 Dec 18 '20 at 19:01
  • I've already addressed all of this, exhaustively. Have a good one! – Asteroids With Wings Dec 18 '20 at 19:19