C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
27: Move Constructors and Move Assignment
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;
}
There are no comments for now.