Sometimes You Must Violate an Abstraction to Maintain It There are instances where code tells a lie in the service of a greater truth.



Last week, I introduced a typical form for overloading functions that deal with objects that might be moved:

void foo(const Thing& t) { // Here, t is a reference to an object that foo can copy but must not modify } void foo(Thing&& t) { //Here, t is a reference to an object that foo can modify freely }

When we call foo , the compiler will select one or the other of these functions. If foo 's argument is an lvalue, the compiler will pick the first version; because that lvalue argument may well be used again later, the body of foo is not permitted to modify the argument. If foo 's argument is an rvalue, the compiler will pick the second version because the compiler knows that the argument will not be used again; the compiler can call the version of foo that is permitted to modify its argument.

We can use this overloading strategy for any function that might behave in two different ways depending on whether it is permitted to modify its argument. However, among the most important such functions are the constructors and assignment operators for the object's own type. A Thing constructor with a Thing argument that it is not permitted to change is, by definition, a copy constructor. If such a constructor is permitted to change its argument, then it is a move constructor.

The whole point of a move constructor is that it knows that its parameter is bound to an argument that will not be used again, so that the constructor can change the argument's value. As an example, let's assume that a Thing object contains a string member:

class Thing { public: Thing(const Thing&); Thing(Thing&&); // … private: string s; // … };

The first Thing constructor is not permitted to modify the string member of its argument, so it must copy the member:

Thing::Thing(const Thing& t): s(t.s) { }

We use t.s to initialize the s member of our own Thing . Doing so calls the string copy constructor, so this initialization involves copying the entire contents of t.s .

What about the second Thing constructor? You might think that you could write

Thing::Thing(Thing&& t): s(t.s) { }

and it would all just work. After all, t is bound to an rvalue, so t.s should be an rvalue, and as a result, the compiler should use the string move constructor to initialize s .

Alas, life is not that simple. Remember that an rvalue reference has two key properties:

It can bind to an rvalue, but

When you actually use it, it behaves as an lvalue

As a result, inside this second constructor, t is an lvalue. As a result, t.s is also an lvalue. What we need is a way to tell the compiler to treat t.s as an rvalue.

One such way is simply to cast t.s to an rvalue:

Thing::Thing(Thing&& t): s(static_cast<string>(t.s)) { }

However, this technique has the unfortunate effect of copying t.s inside the cast — exactly the action we are trying to avoid. However, the cast idea is the right one: What we really need to do is cast t.s to an rvalue reference:

Thing::Thing(Thing&& t): s(static_cast<string&&>(t.s)) { }

This code tells a lie in the service of a greater truth: Although t.s is an lvalue, the cast tells the compiler to pretend that t.s is an rvalue so that the compiler will move the valuef t.s to s rather than copying the value. This pretense is legitimate because the author of the cast knows that t really refers to an rvalue; it is only inside the body of the constructor that t appears to be an lvalue.

As with many casts, this one does not proclaim its purpose. We can make it do so by rewriting it:

Thing::Thing(Thing&& t): s(std::move(t.s)) { }

What std::move really does is to return its argument as an rvalue reference. In effect, every time we use std::move , we are telling a lie. In this case, by writing std::move(t.s) , we are saying that we want to use t.s , but to do so in a way that treats t.s as an rvalue. It is acceptable for us to tell this lie for exactly the same reason that it is acceptable for us to cast t.s to string&& in the previous example: We know that t.s is a member of t , and t really refers to an rvalue in our caller's context.

We can tell such lies any time we are willing to take responsibility for the consequences. As a simple example:

Thing t1, t2; // … t1 = std::move(t2);

This assignment tells the compiler to treat t2 as an rvalue. As a result, the compiler will select Thing 's move-assignment operator ( Thing::operator=(Thing&&) ), which, in turn, will feel free to change the value of t2 arbitrarily. The lie involved in using std::move is harmless so long as the programmer who calls std::move knows that no subsequent code will depend on the value of t2 .

Lies of this kind allow programmers to take advantage of the inner workings of their programs to substitute move operations for copy operations in contexts that the programmers know will be harmless. In particular, it is common for programs to lie in this way when the programmers know that those lies will be invisible from outside. Techniques such as these allow programmers to violate abstractions that they present to users when they know that those violations will not affect the users' code. As another way to view it, when you're implementing an abstraction, you're allowed to break it as long as you repair the damage before your customers see it.

Next week, we'll see some examples of why moving instead of copying is worth the bother.