As we all know, the First Amendment to the C++ Standard states: "The committee shall
make no rule that prevents C++ programmers from shooting themselves in the foot."
Speaking less facetiously, when it comes to choosing between giving programmers
more control and saving them from their own carelessness, C++ tends to err on the side of
giving more control. Being true to that spirit, C++11 allows you to use move semantics not just
on rvalues, but, at your discretion, on lvalues as well. A good example is the std library
function swap . As before, let X be a class for which we have overloaded the
copy constructor and copy assignment operator to achieve move semantics on rvalues.
template<class T> void swap(T& a, T& b) { T tmp(a); a = b; b = tmp; } X a, b; swap(a, b);There are no rvalues here. Hence, all three lines in swap use non-move semantics.
But we know that move semantics would be fine: wherever
a variable occurs as the source of a copy construction or assignment, that variable
is either not used again at all, or else it is used only as the target of an assignment.
In C++11, there is an std library function called template<class T> void swap(T& a, T& b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); } X a, b; swap(a, b);Now all three lines in swap use move semantics. Note that for those types
that do not implement move semantics (that is, do not overload their copy constructor
and assignment operator with an rvalue reference version), the new swap behaves
just like the old one.
Using
std::move , we are in a position to see why the
implementation of
the rvalue reference overload of the copy assignment operator that I showed earlier is still
a bit problematic. Consider a simple assignment between variables, like this:
a = b;What do you expect to happen here? You expect the object held by a to be replaced by
a copy of b , and in the course of this replacement, you expect the object formerly
held by a to be destructed. Now consider the line
a = std::move(b);If move semantics are implemented as a simple swap, then the effect of this is that the objects held by a and b are being exchanged between a and b . Nothing
is being destructed yet. The object formerly held by a will of course be destructed eventually,
namely, when b goes out of scope. Unless, of course, b becomes the target of a
move, in which case the object formerly held by a gets passed on again. Therefore,
as far as the implementer of the copy assignment operator is concerned, it is not known when the
object formerly held by a will be destructed.
So in a sense, we have drifted into the netherworld of non-deterministic destruction here: a variable has been assigned to, but the object formerly held by that variable is still out there somewhere. That's fine as long as the destruction of that object does not have any side effects that are visible to the outside world. But sometimes destructors do have such side effects. An example would be the release of a lock inside a destructor. Therefore, any part of an object's destruction that has side effects should be performed explicitly in the rvalue reference overload of the copy assignment operator: X& X::operator=(X&& rhs) { // Perform a cleanup that takes care of at least those parts of the // destructor that have side effects. Be sure to leave the object // in a destructible and assignable state. // Move semantics: exchange content between this and rhs return *this; } |