Generating move operations (elaborating on Core 1402)

Date: 2012-09-21
Version: N3401=12-0091
Authors: Ville Voutilainen <ville.voutilainen@gmail.com>

Abstract

As Core 1402 states, C++11 is excessively strict about deleting move operations if subobjects have no move operation or the copy operation is non-trivial. Several people have suggested that Core 1402 doesn't go far enough; this paper provides an analysis of the proposed options. The options are, briefly, 1) status quo 2) Core 1402 3) suppress also explicitly defaulted move operations (Merrill) 4) don't suppress nor delete move operations if a moving expression for subobjects is valid (Hinnant). This paper suggests selecting option 4.

N3201 "Moving right along" by Stroustrup (see http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2010/n3201.pdf) is the earlier paper on the subject.

I wish to thank Daniel Krügler for reporting Core 1402 and providing a solution for it, and Jason Merrill, Howard Hinnant and Dave Abrahams for providing elaborated solutions and input for this paper. This paper tries to capture what Merrill and Hinnant have proposed on the core reflector.

Status quo

Core 1402 explains the problem with the status quo:

template<typename T>
 struct wrap {
   wrap() = default;
#ifdef USE_DEFAULTED_MOVE
   wrap(wrap&&) = default;
#else
   wrap(wrap&& w) : t(static_cast<T&&>(w.t)) { }
#endif
   wrap(const wrap&) = default;
   T t;
 };
 struct S {
   S(){}
   S(const S&){}
   S(S&&){}
 };
 typedef wrap<const S> W;
 W get() { return W(); }  // Error, if USE_DEFAULTED_MOVE is defined, else OK

In this example the defaulted move constructor of wrap is selected by overload resolution, but this move-constructor is deleted, because S has no trivial copy-constructor.

Merrill stated the following about status quo:

Any non-static data member or base with no move constructor and a
non-trivial copy constructor causes an explicitly defaulted declaration
to be deleted.

One problem with this is that it is badly specified: the rest of 12.8 was
fixed to talk about the function chosen by overload resolution rather than
what a type "has", but this part was not.

The status quo makes std::map unusable, because map wants to create a
pair<const Key, Val> with a const first type, and a const member cannot be moved.
So the move constructor is deleted, so trying to initialize one of these pairs
from an rvalue is ill-formed.  I don't think we can stick with the status quo.

Core 1402

Merrill stated the following about Core 1402:

The current proposed wording for 1402 changes the existing restrictions
from causing the move to be deleted to instead only suppressing the implicit
declaration.  So the pair<const Key, Val> move constructor is defined,
copies the first member, and moves the second member.

The current proposed wording still suffers from the specification issue above.

Suppressing both implicitly and explicitly defaulted moves

Merrill stated the following:

I'm now wondering if we want to deal with this by treating =default
declarations of move ops the same as implicit declarations: if they
would cause the function to be declared as deleted, instead just
suppress the declaration.

Allowing generated move operations even for non-movable or non-trivially-copyable subobjects

Hinnant proposed the following:

Also consider:  The LWG decided long ago that it is ok for a
move member to throw an exception.  We know how to deal with that and already do.

For simplicity and consistency I would like to see:

1.  An implicit move member behave the same way as an explicitly defaulted one
(and vice-versa).  I believe this invariant is already true for the other
special members.

2.  If for all bases and non-static data members, if the initialization

        X(X&& x) : a_(std::move(x.a_))

is well formed, then the defaulted definition should not be deleted.
I.e. the rules should be expression-based, and should not depend upon what
constructor of a_ actually gets called (a throwing copy constructor is fine,
it will produce a valid throwing move constructor for X).

Comparison of the options

Merrill made the following summary:

All the options concern certain restrictions on defaulted move ctor/op=.

If one of these conditions are found, what happens to implicit/explicitly defaulted move ctor/op=?

Option Implicit Explicit
  1. Status quo
suppress delete
  1. Core 1402
suppress ok
  1. Merrill
suppress suppress
  1. Hinnant
ok ok

Examples

As shown in Core 1402, status quo will lead to a deleted function being chosen by overload resolution if a subobject has no move operation and has a non-trivial copy operation.

A different example is

struct Y {
       Y() = default;
       Y(const Y&) {} /* non-trivial copy, move suppressed */
};

struct X {
       const Y a;
       string b;
       X() : a() {}
       X(X&&) = default; /* would work without this... */
protected:
       X(const X&) = default; /* ...but we need it if we declare a copy operation */
};

X foo() { return X(); }
void foo2(X&& x);

int main()
{
        foo2(foo()); /* can't move X, but chosen by overload resolution */
        X x2 = foo(); /* same here */
}

With Core 1402, this example works, because the defaulted move operation is not deleted, and it'll copy the const Y member and move the string member.

The apparent problem with Core 1402 is that it treats implicitly and explicitly defaulted move operations differently. Explicitly defaulted move operations aren't suppressed even if subobjects have no move and no trivial copy operations, whereas implicitly defaulted move operations are suppressed in those cases. This leads into a situation where, for a class that has a movable and copyable subobject, and a non-trivial copy-only subobject, everything gets copied, but when the move operation is defaulted, the movable and copyable subobject can be moved. Example:

struct Y {
       Y() = default;
       Y(const Y&) {}
};
struct Z {
       Z()=default;
       Z(const Z&) {}
       Z(Z&&) {};
};
struct X {
       const Y a;
       Z b;
       X() : a() {}
};

X foo() {return X();} /* this will copy both X::b and X::a */
void foo2(X&& x) {}

int main()
{
       foo2(foo());
}

If X has a defaulted move, the foo2(foo()); will not copy X::b, but will copy X::a.

Merrill proposes that both implicitly and explicitly defaulted move operations should be suppressed if there's a subobject with no move operation and no trivial copy operation. This makes the implicitly and explicitly defaulted cases do the same thing, which is fallback on copying.

Hinnant points out that any kind of suppression is problematic, because if the move operation is completely suppressed, copy operations will not invoke move operations for subobjects. His example is

struct B {
       B();
       B(const B&);
};

struct A {
       B b;
       std::unique_ptr<int> ptr;
};

A make()
{
      return A();
}

Hinnant elaborates:

If we either delete *or* suppress A's move constructor in this example,
the above code becomes invalid because the return from make() will have
to fall back on A's implicit copy constructor which is deleted because
of the move-only unique_ptr member.

Orthogonal(?) aspect: does noexcept or triviality matter?

Hinnant stated the following:

The LWG decided long ago that it is ok for a move member to
throw an exception. We know how to deal with that and already do.
The rules should be expression-based, and should not depend
upon what constructor of a_ actually gets called (a throwing
copy constructor is fine, it will produce a valid throwing
move constructor for X).

The status quo rules apparently avoid deleting a move operation if a subobject has no move operation but has a trivial copy operation for three reasons: 1) a trivial copy operation will not throw 2) a trivial copy operation is efficient 3) there's already a large amount of examples which lead people to believe that the expression X(X&& x) : a_(std::move(x.a_)) is safe, if they choose to write it explicitly. To deal with a throwing move, the safe alternative would actually be X(X&& x) : a_(std::move_if_noexcept(x.a_)).

It's questionable whether this aspect is truly orthogonal; if we choose to delete or suppress move operations because a copy operation is available, but is non-trivial or throwing, we end up in problematic situations again. Generic wrappers will end up with either 1) deleted moves being resolved to, or 2) copy operations doing just copies, and failing when there's a suitable mixture of moves and copies to be done, if there would be a suitable move but a deleted copy.

Invariants

Abrahams asked whether the solutions proposed can lead to more cases where invariants can be broken. This seems to be the case in an example such as the following:

struct X
{
  X(const X& other) : a(other.a) {} // let's make the copy non-trivial, and the type non-movable in c++11
};

class Y
{
 string a;
 X b;
 void invariant() {assert(!a.empty);}
 // we don't want a destructor, we don't care about checking the invariant in it
 // we don't declare a copy constructor, we think the invariant will hold before and after copying
public:
 Y() : a("foo") {} // establish an invariant
 f() {invariant();} // and check it
};

The status quo rules would suppress move operations for Y, as would Merrill's solution. Hinnant's solution will generate move operations that can break the invariant. For cases where an object of type Y is returned from a function, that doesn't cause problems since the default destructor will not trigger the invariant check, and a user-declared destructor would suppress the move operations. For cases where a move from an lvalue happens, the invariant check can be triggered to assert from outside of class Y.

Conclusion

It seems that whenever we suppress or delete a move operation, we end up in a situation where seemingly valid code ends up ill-formed due to the deleted move operation being resolved to, or can end up ill-formed due to resolving to a possibly deleted copy operation even if a suitable move would be available. Therefore it would seem reasonable to strive for a solution that minimizes deleting or suppressing moves. Hinnant's solution seems to do that. It also has the benefit of treating implicitly and explicitly defaulted move operations the same way, but is superior to Merrill's solution because it doesn't suppress as many moves and does not lead to ill-formed code in the case of a mixture of moves and copies. It also solves the possibly orthogonal issue about whether noexcept or triviality matter, by simply saying they don't receive any special treatment. Finally, despite the invariant concerns mentioned, this paper still proposes Hinnant's solution, because the rule uniformity and teachability is considered to trump the theoretically rare case of breaking invariants.

Wording?

Due to lack of time, there's no wording draft at this point.