(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).
See "BrainExpanded - Introduction" for context on this post. Notes and links Over the years,…
This is the first post, in what I think is going to be a series,…
Back in February, I shared the results of some initial experimentation with a digital twin.…
I am embarking on a side project that involves memory and multimodal understanding for an…
I was in Toronto, Canada. I'm on the flight back home now. The trip was…