Playing with move semantics in C++ – Part 2

(Playing with move semantics in C++ – Part 1)

In this second part, we take a look at what happens when an object stores a reference to another one, effectively not controlling its lifetime. An rvalue reference is required as an argument in order to convey the “I am taking control of the object’s lifetime” semantics. Of course, a shared_ptr or unique_ptr could have also been used and some might prefer it, especially if access to the referred object is still required outside the context of the newly constructed one.

We assume the same ExpensiveToCopy class from part 1. We first introduce the KeepsEtcRef class, instances of which need to refer to an ExpensiveToCopy instance.

class KeepsEtcRef {
 public:
  KeepsEtcRef() = delete;
  KeepsEtcRef(ExpensiveToCopy& etcArg): etc(etcArg) { cout << "KeepsEtcRef: costructor" << endl; }
  ~KeepsEtcRef() { cout << "KeepsEtcRef: destructor" << endl; } 
  void print() { etc.print(); } 
 private:
  ExpensiveToCopy& etc;
};

Let’s instantiate both classes making sure that the ExpensiveToCopy instance doesn’t get destructed while the KeepsEtcRef instance is still around.

{
  cout << "KeepsEtcRef" << endl;
  ExpensiveToCopy etc;
  KeepsEtcRef k(etc);
  k.print();
}
// output
KeepsEtcRef
ExpensiveToCopy (1.0): constructor
KeepsEtcRef: costructor
ExpensiveToCopy (1.0): contains 2 messages
KeepsEtcRef: destructor
ExpensiveToCopy (1.0): destructor

What if the Large instance went away somehow?

{
  cout << "KeepEtcRef outlives ExpensiveToCopy" << endl;
  ExpensiveToCopy* etc = new ExpensiveToCopy();
  KeepsEtcRef k(*etc);
  delete etc;
  k.print();
}
KeepEtcRef outlives ExpensiveToCopy
ExpensiveToCopy (1.0): constructor
KeepsEtcRef: costructor
ExpensiveToCopy (1.0): destructor
ExpensiveToCopy (0.0): contains 0 messages
KeepsEtcRef: destructor

No surprise! The data is gone. The KeepsEtcRef object outlives that to which it maintains a reference. While the program doesn’t crash in this case (since the vector object still exists), something more catastrophic could have happened. Resources were released. We could have ended up with an ugly crash. Can move help us? Let’s rewrite the container class…

class KeepsEtc {
 public:
  KeepsEtc() = delete;
  KeepsEtc(ExpensiveToCopy&& etc): large(move(etc)) { cout << "KeepsEtc: costructor" << endl; }
  ~KeepsEtc() { cout << "KeepsEtc: destructor" << endl; }
  void print() { large.print(); } 
 private:
  ExpensiveToCopy large;
};

Notice the constructor requiring an rvalue, instead of a reference, as an argument.

{
  cout << "KeepsEtc" << endl;
  ExpensiveToCopy etc;
  KeepsEtc k(move(etc));
  k.print();
}
// output
KeepsEtc
ExpensiveToCopy (1.0): constructor
ExpensiveToCopy (1.1): move constructor (cheap)
KeepsEtc: costructor
ExpensiveToCopy (1.1): contains 2 messages
KeepsEtc: destructor
ExpensiveToCopy (1.1): destructor
ExpensiveToCopy (1.0): destructor

No surprises here. Worked like a charm this time :-) We do end up creating a copy of the object but we use the move constructor, which is cheap. What would have happened if we were to explicitly destruct the ExpensiveToCopy object after the move (i.e. not waiting for it to go out of context)?

{
  cout << "KeepsEtc outlives ExpensiveToCopy" << endl;
  ExpensiveToCopy* etc = new ExpensiveToCopy();
  KeepsEtc kl(move(*etc));
  delete etc;
  kl.print();
}
KeepsEtc outlives ExpensiveToCopy
ExpensiveToCopy (1.0): constructor
ExpensiveToCopy (1.1): move constructor (cheap)
KeepsEtc: costructor
ExpensiveToCopy (1.0): destructor
ExpensiveToCopy (1.1): contains 2 messages
KeepsEtc: destructor
ExpensiveToCopy (1.1): destructor

As expected, it worked just fine. That’s because the KeepsEtc was given a copy of the ExpensiveToCopy object, which was cheaply constructed using move().

What would happen if instead of destructing, we were to call a method on the original ExpensiveToCopy object?

{
  cout << "Call to etc after it was moved" << endl;
  ExpensiveToCopy etc;
  KeepsEtc kl(move(etc));
  etc.print();
}
// output
Call to etc after it was moved
ExpensiveToCopy (1.0): constructor
ExpensiveToCopy (1.1): move constructor (cheap)
KeepsEtc: costructor
ExpensiveToCopy (1.0): contains 0 messages
KeepsEtc: destructor
ExpensiveToCopy (1.1): destructor
ExpensiveToCopy (1.0): destructor

Again, I was lucky the process didn’t crash. The implementation of the vector move constructor didn’t leave the vector data member in an undefined state. However, the data was moved.

All code is available at… https://github.com/savas/playground/tree/master/cpp-move-semantics

(Thanks to Viswanath Sivakumar for spotting a copy-paste mistake with the last code snippet. Now fixed).