Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 8

Optimizing with Move Semantics

Recommended resources

In our last lesson, we established the crucial distinction between lvalues and rvalues. This laid the theoretical groundwork for a major C++11 performance feature: move semantics. You learned that by identifying temporary objects (rvalues), we open up an opportunity to avoid expensive copies and instead "steal" their resources.

Today, we will build on that foundation. This lesson is about implementing the very mechanisms that perform this "stealing": the move constructor and the move assignment operator. Mastering these will allow you to write classes that manage resources—like the memory for a complex user stroke or a large bitmap in your whiteboard application—with maximum efficiency, directly addressing your project's performance requirements.

The Cost of Copying

First, let's briefly revisit why we need to avoid copying. For any class that manages a dynamic resource (like memory allocated with new), a copy operation must perform a "deep copy." This involves two expensive steps:

  1. Allocating a completely new block of memory.
  2. Copying the data from the old block to the new one.

For a large object, this is a significant performance hit. Move semantics provides an alternative by simply transferring ownership of the existing resource.

This diagram perfectly illustrates the concept. A **copy** duplicates the underlying resource (Memory block M1 is copied to M2), resulting in two separate, owned resources. A **move**, however, transfers the ownership of the original resource (Memory block M1) from Object A to Object B, leaving Object A empty. This avoids a new allocation and a data copy.

The Move Constructor: Building by Stealing

The move constructor is a special member function that the compiler calls to create a new object from a temporary (an rvalue). Its signature is ClassName(ClassName&& other) noexcept. The && tells the compiler to select this overload for rvalue arguments.

The implementation follows a simple, three-step pattern:

  1. Transfer Resource Ownership: Perform a shallow copy of the resource handle (e.g., the pointer) and any associated metadata (e.g., size) from the source object (other) to the new object (this).
  2. Nullify the Source: Set the source object's resource handle to nullptr and its metadata to zero. This is the "stealing" part. It ensures the source object, other, no longer owns the resource.
  3. Leave the Source Destructible: The source object is now in a valid but "hollow" state. When its destructor is called, it will try to delete a null pointer, which is a safe and well-defined no-op. This prevents a catastrophic double-free error.

The result is an O(1) operation—a few pointer assignments—that is dramatically faster than a deep copy.

The following video from The Cherno provides an excellent, practical walkthrough of implementing a move constructor for a custom String class.

Move Semantics in C++

This clip from "Move Semantics in C++" demonstrates the full implementation of a move constructor.

Watch the section from implementing the move constructor. Pay close attention to how the other object's data members are set to nullptr and 0 after its pointer has been "stolen." This is the key to leaving the temporary object in a safe, destructible state.

The Importance of noexcept

You'll notice the noexcept specifier in the move constructor's signature. This is not merely a suggestion; it's critical for performance. Standard library containers like std::vector need to move elements around when they reallocate their internal storage (e.g., during a push_back that exceeds capacity). To provide a strong exception guarantee, if an element's move constructor can throw an exception, std::vector will refuse to use it. Instead, it will fall back to the slower (but safe) copy constructor. By marking your move operations noexcept, you guarantee to the compiler that they are safe to use in these high-performance scenarios.

Copy/Move Semantics and the Rule of Five | EduC++

The "How It Works Deep Dive" in this article from EduC++ gives a concise and powerful explanation of the move constructor's mechanics and the critical role of noexcept.

Please read the section titled 6 Move constructor. The explanation under "WHY NOEXCEPT" is particularly important for anyone focused on system performance.


The Move Assignment Operator: Stealing into an Existing Object

What if you want to move into an object that already exists? This is the job of the move assignment operator. Its signature is ClassName& operator=(ClassName&& other) noexcept.

It's similar to the move constructor, but with one key difference: the this object already exists and may own a resource that needs to be freed.

The implementation steps are:

  1. Check for Self-Assignment: First, ensure the source and destination are not the same object (if (this != &other)). Moving an object to itself would lead to prematurely deleting its own resource.
  2. Release Existing Resource: delete[] the resource currently owned by this.
  3. Transfer Resource Ownership: Just like in the move constructor, copy the pointer and size from other to this.
  4. Nullify the Source: Set other's pointer to nullptr.
  5. Return *this: As is standard for assignment operators.

The Microsoft Learn documentation provides a clear, step-by-step procedure for writing both special members.

Move Constructors and Move Assignment Operators (C++)

This article provides a structured, procedural guide to implementing move semantics. The MemoryBlock class is a perfect canonical example.

Focus on the two main procedures. First, read the steps for the move constructor. Then, read the steps for the move assignment operator. Finally, look at the complete implementation which incorporates the noexcept specifier.

For a more dynamic explanation, The Cherno again provides an excellent video walkthrough.

std::move and the Move Assignment Operator in C++

This clip from "std::move and the Move Assignment Operator in C++" focuses specifically on implementing the move assignment operator.

Watch the segment from Move assignment operator. The video clearly explains the need to delete the existing resource and handle self-assignment, which are the two key differences from a move constructor.


The Rule of Five: Tying It All Together

In pre-C++11, we had the "Rule of Three": if you write a destructor, copy constructor, or copy assignment operator, you should probably write all three. With the addition of move semantics, this has been updated to the Rule of Five.

The Rule of Five: If you declare any of the five special member functions (destructor, copy constructor, copy assignment operator, move constructor, move assignment operator), you should declare or =delete all five.

Why is this so important? The C++ compiler is helpful, but it has rules about when it will automatically generate these functions. Specifically, if you declare a custom copy constructor, copy assignment operator, or destructor, the compiler will NOT automatically generate the move constructor and move assignment operator.

This chart illustrates how the compiler decides whether to generate special member functions. Notice the red 'x' marks in the "Move-ctor" and "Move-op=" columns. If you write a copy constructor, copy assignment operator, or destructor, the compiler suppresses move semantics. Your class will silently fall back to copying, negating your performance efforts.

This is a subtle but deadly trap for performance. You might write a destructor and assume move semantics will "just work," only to find your program is making expensive copies everywhere. Therefore, for any class that manages a raw resource, you must be explicit about all five functions to ensure correct and performant behavior.


Conclusion

You have now moved from the theory of value categories to the practical implementation of move semantics. By writing move constructors and move assignment operators, you can create classes that transfer ownership of heavy resources efficiently, a technique fundamental to high-performance C++ and essential for your whiteboard project.

Key Takeaways:

  • Move Constructor: Initializes a new object by "stealing" the resources from a temporary (rvalue) object.
  • Move Assignment Operator: Updates an existing object by releasing its own resources and then stealing them from a temporary object.
  • The Core Pattern: Both operations involve a shallow copy of the resource handle (pointer) followed by nullifying the source object to prevent double-frees.
  • noexcept is Crucial: Move operations must be marked noexcept to guarantee they will be used by standard library containers like std::vector, avoiding silent performance degradation.
  • The Rule of Five: For any class that manages a raw resource, you must explicitly define, default, or delete all five special member functions (destructor, copy-ctor, copy-assign, move-ctor, move-assign) to prevent the compiler from silently disabling move semantics.

In this lesson, we saw that move operations are automatically invoked for rvalues (temporaries). But what if you have an lvalue—a named object—that you are done with and want to move from? In our next lesson, we will cover std::move, the standard library function that allows you to explicitly cast an lvalue to an rvalue, giving you full control over when and where moves occur.