6

In C++11, one can explicitly default a special member function, if its implicit generation was automatically prevented.

However, explicitly defaulting a special member function only undoes the implicit deletion caused by manually declaring some of the other special member functions (copy operations, destructor, etc.), it does not force the compiler to generate the function and the code is considered to be well formed even if the function can't in fact be generated.

Consider the following scenario:

struct A
{
    A ()         = default;
    A (const A&) = default;
    A (A&&)      = delete;  // Move constructor is deleted here
};

struct B
{
    B ()         = default;
    B (const B&) = default;
    B (B&&)      = default; // Move constructor is defaulted here

    A a;
};

The move constructor in B will not be generated by the compiler, because doing so would cause a compilation error (A's move constructor is deleted). Without explicitly deleting A's constructor, B's move constructor would be generated as expected (copying A, rather than moving it).

Attempting to move such an object will silently use the copy constructor instead:

B b;
B b2 (std::move(b)); // Will call B's copy constructor

Is there a way to force the compiler into either generating the function or issue a compilation error if it can't? Without this guarantee, it's difficult to rely on defaulted move constructors, if a single deleted constructor can disable move for entire object hierarchies.

Jan Holecek
  • 2,131
  • 1
  • 16
  • 26
  • Shouldn't you know if the members you are including are movable or not? – NathanOliver Feb 04 '16 at 15:21
  • 3
    They may be movable when the class was originally implemented, however, if any more members are added later, the movable requirement could be overlooked (especially if they are added by someone else). – Jan Holecek Feb 04 '16 at 15:22
  • 2
    It should be noted that a type that deletes its move constructor but *not* its copy constructor is very... bizarre. There is absolutely nothing to be gained by doing this. So it may be best to ignore this case, considering it to be the result of someone doing something idiotic for no reason. – Nicol Bolas Feb 04 '16 at 17:21
  • @JanHolecek: For futher details on this, see [this question](http://stackoverflow.com/questions/33988934). – Nicol Bolas Feb 04 '16 at 17:28
  • 1
    Define the function outside the class as defaulted. – David G Feb 04 '16 at 18:13
  • @NicolBolas: Indeed, such a code doesn't seem very likely... – Jan Holecek Feb 09 '16 at 16:33
  • @0x499602D2: That does the trick! As Nicol Bolas said, this isn't a very realistic scenario, but nevertheless this provides a solution to the problem. Feel free to add a full fledged answer. – Jan Holecek Feb 09 '16 at 16:37

1 Answers1

4

There is a way to detect types like A. But only if the type explicitly deletes the move constructor. If the move constructor is implicitly generated as deleted, then it will not participate in overload resolution. This is why B is movable even though A is not. B defaults the move constructor, which means it gets implicitly deleted, so copying happens.

B is therefore move constructible. However, A is not. So it's a simple matter of this:

struct B
{
    static_assert(is_move_constructible<A>::value, "Oops...");

    B ()         = default;
    B (const B&) = default;
    B (B&&)      = default; // Move constructor is defaulted here

    A a;
};

Now, there is no general way to cause any type which contains copy-only types to do what you want. That is, you have to static assert on each type individually; you can't put some syntax in the default move constructor to make attempts to move B fail.

The reason for that has to do in part with backwards compatibility. Think about all the pre-C++11 code that declared user-defined copy constructors. By the rules of move constructor generation in C++11, all of them would have deleted move constructors. Which means that any code of the form T t = FuncReturningTByValue(); would fail, even though it worked just fine in C++98/03 by calling the copy constructor. So the move-by-copy issue worked around this by making these copy instead of moving if the move constructor could not be generated.

But since = default means "do what you would normally do", it also includes this special overload resolution behavior that ignores the implicitly deleted move constructor.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Nice answer. It is good to keep in mind that for `struct A` if only the copy constructor was present and the move constructor was not explicitly deleted, then its move constructor would have been implicitly deleted. In this case the `static_assert` involving `is_move_constructible` would not fail. – Hari Jun 21 '23 at 08:58