-
Notifications
You must be signed in to change notification settings - Fork 8
Non Proposal for Destructive Move
[This is off-the-top-of-my-head ruminations on what a destructive move addition might look like for C++1z. I do realize that there are likely many issues here that I'm missing.]
An efficient basis operation for move is a destructive-move-constructor (dmove). We want such an operation to be known by the compiler so it can be used implicitly (see below for when it is used). I would propose the following syntax:
class some_class {
public:
~some_class(some_class&& source);
};
The dmove operation must not throw an exception (throwing an exception from a dmove would be undefined behavior). The default implementation dmoves all members. There is no default if the members are not dmovable. dmoving a POD type is a simple copy.
The dmove operation leaves the source in a destructed state, as raw memory. The semantics of dmove is that the constructed object will contain the prior value of the source object.
When constructing a value from an rvalue of a class with a dmove, the compiler will invoke the dmove operation or elide the operation entirely.
some_type f();
some_type x = f(); // will invoke dmove or elide
Returning a local variable, or passed-by-value argument, with a dmove from a function will either invoke dmove or elide the operation entirely.
some_type f() {
some_type result;
//...
return result; // will invoke dmove or elide
}
If the last reference to a local variable, or passed-by-value argument, is a copy operation which is reachable along all code paths, except via exceptions, through the function, then the copy operation is replaced with a dmove operation. Note that this is a change to the destruction order of local variables.
class example {
some_type member;
public:
example(some_type x) : member(x) // dmove invoked
{ }
};
void f1() {
some_type x;
//... does not contain any return statements
some_type y = x; // y dmove invoked, x is now destructed
}
void f2() {
some_type x;
//... does not contain any return statements
if (p) {
some_type y = x; // copy invoked, even though no further references to x
}
}
dmove assignment is achieved by having an assignment operation that passes by value and is assumed and required to be noexcept.
class some_class {
public:
~some_class(some_class&& source);
some_class& operator=(some_class source);
};
When a class has a dmove constructor, a default assignment operator is provide which is pass by value and noexcept. The default implementation is equivalent to:
some_class& operator=(some_class source) noexcept {
this->~some_class;
new (this) some_class(source); // by implicit rules this is a dmove
return *this;
}
Because of the above rules, the need to explicitly invoke a dmove is much rarer than with explicit moves.
An rvalue reference cast, such as the existing std::move(), may cause the invocation of the dmove operation, however, this will leave the source object in a destructed state.
For compatibility, std::move() should be redefined to be equivalent to a dmove followed by default construction of the moved from object when the moved from object is not an rvalue. To be backward compatible with the existing move, a type providing a dmove operation must also supply a default ctor. The default ctor need only to leave the object in a partially formed state.
This definition of std::move allows existing code to remain unchanged. For example, an existing swap() implementation would do this:
template <class T>
void swap(T& x, T& y) {
T tmp = move(x); // dmove of x, followed by default construction of x
x = move(y); // dmove of y, followed by default construction of y
y = move(tmp); // dmove of tmp into y, followed by default construction of tmp.
}
[Code that currently uses rvalue casts directly, rather than going through std::move() may silently break in the presence of a dmove type. - I do not currently have a solution to this issue nor have I done any research to scope the size of the issue though searching for explicit rvalue casts in code should be simple.]
A new operation, std::move_unsafe(), is introduced that does an rvalue cast for dmove types. [Open issue, it could be defined to do a move followed by a destruction for non-dmove types. More experience is needed here.]
Using this we can define an efficient swap. Note that the unsafe operations are only needed for the non-local variables.
template <class T>
void swap(T& x, T& y) {
T tmp = move_unsafe(x);
new (&x) T(move_unsafe(y));
new (&y) T(tmp);
}
Alternatively, it may be a bit more direct to use aligned storage with move_unsafe operations:
template <class T>
void swap(T& x, T& y) {
aligned_storage_t<T> tmp;
new (&tmp) T(move_unsafe(x));
new (&x) T(move_unsafe(y));
new (&y) T(move_unsafe(tmp);
}
Both implementations are equivalent.
In a similar way, move_unsafe() can be used for the other permutation algorithms, and for data structure operations such as resizing, or inserting into, a vector.
A dmove type with a copy constructor, is a dmove only type. Such a type can still be passed by value, returned from a function, or assigned to, so long as the operation is one of the required implicit dmove operations.
A dmove only type is more restrictive than a move only type, but also safer since it cannot be referenced after having been implicitly moved from.
Passing arguments by rvalue reference present a dilemma. There are two possible approaches:
- The compiler assumes anytime a dmoveable value is passed as an rvalue reference it will be consumed.
- The compiler treats dmoveable values as it does other values, and a consuming function must leave it in a destructible state.
Option 1 has many issues with backward compatibility and may run into aliasing issues as well. It also has trouble with perfect forwarding where the forwarding call may not be forwarding the rvalue to a consuming call.
Because of those drawbacks, I believe option 2 is the only viable option. Generally, since sink values should be passed by value the need for passing by rvalue reference should be rare, however, it does mean that perfect forwarding will not be "perfect", though no worse than it is today, but will require the object either also have a move-ctor or a default-ctor to simulate the current move semantics.
Alex Stepanov first proposed to me the idea of a "move destructor", but neither of us saw a way to make it work within the language. Alex and Paul McJones defined partialy formed object states in Elements of Programming (section 1.5) as the requirements for a default constructed type. Over the last decade, several people have influenced my thinking on moving object, especially Dave Abrahams, Howard Hinnant, and Andrei Alexandrescu. Matt Calabrese provided a critique of this post here https://github.com/mcalabrese/cpp-stuff/wiki/std::move-Is-Not-Destructive-Move which was helpful in simplifying and refining this non-proposal.