Move Semantics in Modern C++ (2)

Now that we’ve looked at how std::move() can be applied to Standard Library types, such as std::string, in this article we’re going to look at creating our own “move-aware” type. We want it to have at least one member for which moving is potentially cheaper than copying, and for this we’ll use a raw char* pointer. We also want to log all uses of the five special member functions (constructor, copy-constructor, move-constructor, copy-assignment, and move-assignment) in order to understand exactly what is happening.

The first part of our class looks like this:

 class Movable { char *str; const std::size_t id; inline static std::size_t index{}; inline static std::ostream& os = std::cerr; 

The member str is intended to point to a character string, which would need to be null-terminated (an NTMBS), while id will hold a unique number for us to be able to track object lifetimes. The static (per-class) variable index is used to keep a count of the number of Movables created, while os is a reference to the output stream used for logging.

Next comes a function to perform character string duplication (in the style of strdup() from POSIX):

 class Movable { // ... char *dup(const char *s1) { auto len = strlen(s1); auto s2 = new char[len + 1]; memcpy(s2, s1, len + 1); return s2; } 

Now we can proceed to creating the class interface. The default constructor needs to be overridden to allow creation of “empty” Movables, and a constructor taking a const char* must be provided:

 class Movable { // ... public: Movable() : str{ nullptr }, id{ ++index } { os << "Construct empty Movable with id " << id << '\n'; } Movable(const char *s) : str{ dup(s) }, id{ ++index } { os << "Construct Movable with id " << id << " and content: " << str << '\n'; } 

For the constructors, str is initialized with either nullptr or a deep copy of the parameter. In both cases, id is initialized from an incremented index, and a log message is output.

For the copy-constructor, str is initialized with a deep copy of rhs.str, while for the move-constructor, this pointer is “stolen” and rhs.str is set to nullptr. The id member is initialized as for the previous two constructors:

 class Movable { // ... Movable(const Movable& rhs) : str{ dup(rhs.str) }, id{ ++index } { os << "Copy-construct Movable with id " << id << " from id " << rhs.id << '\n'; } Movable(Movable&& rhs) : str{ rhs.str }, id{ ++index } { rhs.str = nullptr; os << "Move-construct Movable with id " << id << " from id " << rhs.id << '\n'; } 

Note that the parameter type for the Movable move-constructor is not const, as it needs to be updated.

The copy-assignment and move-assignment operators share some symmetry with this approach, with the key difference being that str must be deleted and ids are not created or changed:

 class Movable { // ... Movable& operator=(const Movable& rhs) { char *str1 = dup(rhs.str); delete[] str; str = str1; os << "Copy-assign Movable with id " << id << " from id " << rhs.id << '\n'; return *this; } Movable& operator=(Movable&& rhs) { delete[] str; str = rhs.str; rhs.str = nullptr; os << "Move-assign Movable with id " << id << " from id " << rhs.id << '\n'; return *this; } 

Finally, the destructor simply deletes member str, while a print function is provided to show the state of a Movable object:

 class Movable { // ... ~Movable() { delete[] str; } void print(std::ostream& out = std::cout) { out << id << ": "; if (str) { out << str << '\n'; } else { out << "(No data)\n"; } } }; 

Now we’re ready to start writing code which uses our new class! We want to create four objects from unique strings, and then experiment with assigning them to other objects, making sure we use all of the special member functions from above. Here is the complete program:

 #include "Movable.hpp" int main() { Movable m1{ "Apple "}, m2{ "Banana "}, m3{ "Clementine "}, m4{ "Date" }; Movable m11{ m1 }, m12{ std::move(m2) }, m13, m14; m13 = m3; m14 = std::move(m4); m1.print(); m2.print(); m3.print(); m4.print(); m11.print(); m12.print(); m13.print(); m14.print(); } 

Notice that objects m1 through m4 are created from strings, which objects m11 through m14 are created from these previously-created objects. The output from running this code is:

 Construct Movable with id 1 and content: Apple Construct Movable with id 2 and content: Banana Construct Movable with id 3 and content: Clementine Construct Movable with id 4 and content: Date Copy-construct Movable with id 5 from id 1 Move-construct Movable with id 6 from id 2 Construct empty Movable with id 7 Construct empty Movable with id 8 Copy-assign Movable with id 7 from id 3 Move-assign Movable with id 8 from id 4 1: Apple 2: (No data) 3: Clementine 4: (No data) 5: Apple 6: Banana 7: Clementine 8: Date 

The most important thing to note is that objects m2 and m4 are left empty, but in such a state as to satisfy the class invariant (can be safely deleted or re-assigned to). This is proof that the effect of std::move() is to instruct the compiler to invoke the move-aware constructor or assignment operator for these two objects respectively.

Of course, reassigning objects within the same scope is of limited usefulness, so in the next article we’ll progress to calling functions with parameter types of Movable, const Movable& and Movable&&, making a note of the types of move or copy that they invoke (especially for temporary, or “r-value”, objects).

2 thoughts on “Move Semantics in Modern C++ (2)”

  1. Your copy constructor isn’t exception-safe. If dup throws, you’ve already deleted str, so the Movable object will no longer be in a valid state, its invariants are not preserved. One possible way to make it exception safe is:

    Movable& operator=(const Movable& rhs) {    char* s1 = dup(rhs.str);    delete[] str;    str = s1;    os     return*this;}

    In this version if dup throws, the Movable object is left in a valid state. In fact its state would be unchanged, so I believe this would provide the strong exception guarantee.

    Like

    1. Thank you for the correction, the code as written isn’t exception safe (a major oversight) and I’ve updated the article.

      Also, your version does provide the strong exception guarantee (operation is atomic), which could also be achieved by specializing `std::swap()` and using the copy-and-swap idiom. However in the interest of keeping simple, I haven’t changed the `Movable` class to this alternative.

      Like

Leave a comment