Skip to Content
Course content

27: Move Constructors and Move Assignment

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

A few years ago, I was reviewing a pull request for a colleague who was building a custom physics engine. He had this ParticleSystem class that managed a massive heap-allocated array of vertex data. Everything worked fine in isolation, but as soon as he started passing these systems around—returning them from factory functions or storing them in a std::vector—the frame rate plummeted. He was baffled because he wasn't explicitly calling copy anywhere. What he didn't realize was that every time the vector resized or a temporary object was returned, C++ was dutifully performing a "deep copy" of several megabytes of data, only to immediately destroy the original. It was a textbook case of wasting CPU cycles on data that was just about to die.

This is where move semantics come in. Instead of duplicating the data, we "steal" it. If an object is a temporary (an r-value), there's no point in copying its contents; we can just grab the pointer to the memory and leave the old object in a valid, empty state. It's the difference between photocopying a 500-page book and simply taking the book off someone's desk because they told you they were throwing it away.

Stealing Resources with the Move Constructor

To implement this, we use the move constructor. You've already seen r-value references (&&), but here they serve as a signal: "I am allowed to gut this object." The key is to copy the pointer from the source object to the new one and then—this is the part developers often forget—null out the pointer in the source object.

class DynamicBuffer {
    size_t size;
    int* data;

public:
    // Standard constructor
    DynamicBuffer(size_t s) : size(s), data(new int[s]) {}

    // Destructor
    ~DynamicBuffer() { delete[] data; }

    // Move Constructor
    DynamicBuffer(DynamicBuffer&& other) noexcept 
        : data(other.data), size(other.size) {
        
        // The "Steal": Leave the original in a clean state
        other.data = nullptr;
        other.size = 0;
    }
};

Notice the noexcept. I cannot stress this enough: always mark your move constructors noexcept. If you don't, std::vector will often revert to using the copy constructor during reallocations because it wants to guarantee a strong exception safety guarantee. If you omit it, you're essentially telling the compiler, "I'd like to move, but I might crash while doing it," and the compiler will play it safe by copying everything anyway.

Handling the Cleanup in Move Assignment

Move assignment is slightly more complex because the object already exists. It might already be holding onto its own memory. If you just steal the new pointer, you've just created a memory leak with your own original data. You have to clean up your own house before you move into the new one.

The most robust way to handle this is to check for self-assignment (though it's rare with r-values) and then swap or release the current resource.

DynamicBuffer& operator=(DynamicBuffer&& other) noexcept {
    if (this != &other) {
        // 1. Clean up existing resource
        delete[] data;

        // 2. Steal the resource
        data = other.data;
        size = other.size;

        // 3. Null out the source
        other.data = nullptr;
        other.size = 0;
    }
    return *this;
}

I've seen some people use the "copy-and-swap" idiom here, which is elegant, but for a pure move assignment, a direct steal is the most performant. Just remember: delete the old, grab the new, null the source. If you miss that last step, the source object's destructor will run when it goes out of scope and delete[] the memory you just stole, leaving you with a dangling pointer and a very bad afternoon of debugging.




📋 Practical Task

Implementing a Move-Aware ImageBuffer

You are tasked with optimizing a class called ImageBuffer that handles large raw pixel arrays. Currently, the class only has a copy constructor, causing massive performance hits during image processing pipelines.

Your Goal: Implement the Move Constructor and the Move Assignment Operator to ensure that pixel data is transferred rather than copied when dealing with temporary ImageBuffer objects.

#include <iostream>
#include <utility>

class ImageBuffer {
private:
    size_t width;
    size_t height;
    unsigned char* pixels;

public:
    ImageBuffer(size_t w, size_t h) : width(w), height(h), pixels(new unsigned char[w * h]) {
        std::cout < "Allocating buffer\n";
    }

    ~ImageBuffer() {
        delete[] pixels;
        std::cout < "Deleting buffer\n";
    }

    // TODO: Implement Move Constructor
    // Remember to use noexcept and null out the source pixels pointer.

    // TODO: Implement Move Assignment Operator
    // Remember to handle self-assignment and delete existing pixels.

    // Disable copying for this exercise to force the use of move semantics
    ImageBuffer(const ImageBuffer&) = delete;
    ImageBuffer& operator=(const ImageBuffer&) = delete;

    void printInfo() {
        if (pixels) std::cout < "Buffer active: " < width < "x" < height < "\n";
        else std::cout < "Buffer is empty\n";
    }
};

int main() {
    ImageBuffer a(1920, 1080);
    std::cout < "Object a: "; a.printInfo();

    // This should trigger the move constructor
    ImageBuffer b = std::move(a); 
    
    std::cout < "Object a after move: "; a.printInfo();
    std::cout < "Object b after move: "; b.printInfo();

    return 0;
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.