Skip to Content
Course content

87: Move Semantics and Rvalue References

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you something that fundamentally changed how I write C++. For a long time, I just accepted that copying large objects was the cost of doing business. But as our projects grew, those "hidden" copies started killing performance. Let's look at a classic scenario: managing a large chunk of heap memory.

The Cost of Being Careful

Let's start with a simple DynamicBuffer class. I've written it to be "safe," meaning it follows the Rule of Three. It manages a raw pointer to a heap-allocated array.

#include <iostream>
#include <algorithm>

class DynamicBuffer {
    size_t size;
    int* data;

public:
    DynamicBuffer(size_t s) : size(s), data(new int[s]) {
        std::cout < "Allocating " < size < " elements\n";
    }

    ~DynamicBuffer() {
        delete[] data;
        std::cout < "Destroying buffer\n";
    }

    // Copy Constructor
    DynamicBuffer(const DynamicBuffer& other) : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
        std::cout < "Deep copying " < size < " elements\n";
    }
};

DynamicBuffer createBigBuffer() {
    return DynamicBuffer(10000); 
}

int main() {
    DynamicBuffer myBuf = createBigBuffer();
    return 0;
}

If you run this, you might see "Allocating", "Deep copying", and then "Destroying" (twice). Wait, why are we copying? The object inside createBigBuffer is a temporary. It's about to be destroyed anyway. Why on earth are we spending CPU cycles duplicating 10,000 integers just to immediately delete the original?

Identifying the "Disposable" Object

The problem is that our copy constructor takes a const DynamicBuffer&. This is an lvalue reference. It says, "I can take any buffer that has a name and a persistent address." It doesn't care if the buffer is a permanent variable or a temporary object about to vanish.

We need a way to tell the compiler: "This object is a temporary (an rvalue). You can steal its resources because it's dying anyway." This is where the double-ampersand && comes in.

Stealing the Goods

Let's add a Move Constructor. Instead of allocating new memory, we're going to literally steal the pointer from the other object and set the other object's pointer to nullptr so its destructor doesn't accidentally free our stolen memory.

// Move Constructor
DynamicBuffer(DynamicBuffer&& other) noexcept : size(other.size), data(other.data) {
    std::cout < "Moving " < size < " elements\n";
    
    // The "Theft": Leave the source in a valid but empty state
    other.data = nullptr;
    other.size = 0;
}

I added noexcept there because STL containers (like std::vector) won't use your move constructor unless they are sure it won't throw an exception. It's a quirk of the library, but a crucial one to remember.

Now, if I run the main function again, the output changes. Instead of "Deep copying", I see "Moving". We didn't allocate any new memory; we just swapped a pointer. The performance gain for large buffers is astronomical.

Forcing a Move with std::move

Here is where it gets tricky. What if the object isn't a temporary? What if it's a named variable that I just happen to know I don't need anymore?

DynamicBuffer bufA(5000);
// DynamicBuffer bufB = bufA; // This calls the COPY constructor. bufA still exists.
DynamicBuffer bufB = std::move(bufA); // This calls the MOVE constructor.

I often see people think std::move actually moves something. It doesn't. It's essentially a cast. It tells the compiler: "I know bufA is an lvalue, but please treat it as an rvalue so the move constructor can be triggered."

After that line, bufA is still a valid object, but it's an empty shell. If you try to access bufA.data, you'll hit a null pointer. You've effectively transferred ownership of the heap memory from A to B.

The Logic Recap

When I'm deciding whether to implement this, I follow this mental flow:

  • Does my class own a resource (heap memory, file handle, socket)? If no, I don't need move semantics.
  • Am I doing expensive deep copies of temporaries? If yes, I need a move constructor.
  • Did I null out the source object? If no, I'll have a double-free crash when the temporary's destructor runs.



📋 Practical Task

Implementing a Move-Aware TextureWrapper

You are building a graphics engine. You have a TextureWrapper class that manages a handle to a GPU texture (simulated here with an integer ID and a large metadata buffer). Currently, the class only supports deep copying, which is causing stuttering during scene transitions.

Your Task:

  1. Implement a Move Constructor that transfers the textureId and the metadata pointer from the source object to the new object.
  2. Ensure the source object is left in a state where its destructor will not attempt to free the stolen metadata (set the pointer to nullptr).
  3. Implement a Move Assignment Operator (operator=) to handle cases where a texture is moved into an already existing TextureWrapper object. Remember to clean up the existing resource before stealing the new one!
#include <iostream>
#include <cstring>

class TextureWrapper {
    int textureId;
    char* metadata;

public:
    TextureWrapper(int id, const char* info) : textureId(id) {
        metadata = new char[strlen(info) + 1];
        strcpy(metadata, info);
        std::cout < "Texture " < id < " created.\n";
    }

    ~TextureWrapper() {
        delete[] metadata;
        std::cout < "Texture resource cleaned up.\n";
    }

    // Copy Constructor (Keep this as is)
    TextureWrapper(const TextureWrapper& other) : textureId(other.textureId) {
        metadata = new char[strlen(other.metadata) + 1];
        strcpy(metadata, other.metadata);
        std::cout < "Texture " < textureId < " deep copied.\n";
    }

    // TODO: Implement Move Constructor here
    
    // TODO: Implement Move Assignment Operator here
};

int main() {
    TextureWrapper tex1(101, "DiffuseMap_Albedo");
    
    // This should trigger the move constructor
    TextureWrapper tex2 = std::move(tex1); 
    
    TextureWrapper tex3(102, "NormalMap_Detail");
    // This should trigger the move assignment operator
    tex3 = std::move(tex2); 

    return 0;
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.