When Copying Costs Too Much: Using Move Semantics in C++ for Zero‑Overhead Resource Transfer
Learn how C++ move semantics replace expensive copies with cheap pointer transfers, see a concrete Buffer example, and verify moves with compiler flags and sanitizers.
14 Jan 2026, 11:01 UTC

The problem: unnecessary copies of heavy objects
Imagine a graphics engine that creates a temporary std::vector holding millions of vertex positions each frame. If the function that builds the vector returns it by value, the naïve implementation would copy the entire buffer into the caller’s storage, wasting CPU cycles and memory bandwidth.
Thesis: move semantics let the compiler transfer ownership instead of copying data
By declaring a move constructor and move assignment operator (or relying on the compiler‑generated ones for trivially movable types), we tell C++ that an rvalue can safely give up its resources. The source object is left in a valid but unspecified state, avoiding deep copies while keeping the program correct.
How rvalue references enable move semantics
An rvalue reference, written T&&, binds only to temporary objects (or to std::move‑cast lvalues). Overload resolution prefers these functions when the argument is an rvalue, so a move constructor Buffer(Buffer&&) is selected instead of the copy constructor Buffer(const Buffer&).
Worked example: a simple movable buffer
#include
#include
#include
class Buffer {
public:
explicit Buffer(std::size_t size) : size_(size), data_(new int[size]) {
std::cout << "Buffer ctor, size=" << size_ << '\n';
}
// copy constructor (deep copy)
Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
std::cout << "Buffer copy ctor\n';
std::copy(other.data_, other.data_ + other.size_, data_);
}
// move constructor (steal resources)
Buffer(Buffer&& other) noexcept : size_(other.size_), data_(other.data_) {
std::cout << "Buffer move ctor\n';
other.size_ = 0;
other.data_ = nullptr; // leave source in a valid but unspecified state
}
// copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
std::copy(other.data_, other.data_ + size_, data_);
}
return *this;
}
// move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
}
return *this;
}
~Buffer() { delete[] data_; }
std::size_t size() const { return size_; }
private:
std::size_t size_;
int* data_;
};
Buffer make_buffer(std::size_t n) {
return Buffer(n); // return by value
}
int main() {
Buffer b1 = make_buffer(1'000'000); // move constructor invoked
Buffer b2 = b1; // copy constructor invoked (intentional)
b2 = std::move(b1); // move assignment invoked
// After the move, b1 is safe to destroy or re‑initialize
}
When compiled with optimizations (-O2) the move constructor merely copies the pointer and size, avoiding a memcpy of one million integers.
Trade‑offs and limitations
- Valid but unspecified state: After a move, the source object must not be read as if it still held its original resources. Re‑initializing or assigning a new value is required before reuse.
- Multiple moves: Moving from the same object twice without re‑initialization can lead to double‑free or use‑after‑move bugs.
- Compiler elision: With NRVO or return‑value optimization the move constructor may be omitted entirely, making it hard to observe moves in a debug build. Use
-fno-elide-constructorsto force the move for testing.
How to verify that moves are happening
- Compile the example with tracing enabled:
g++ -std=c++17 -O0 -fno-elide-constructors -g example.cpp -o example. Run the program; you should see "Buffer move ctor" printed for the return frommake_bufferand for thestd::moveassignment. - Run under AddressSanitizer to catch illegal use after a move:
g++ -fsanitize=address -fno-elide-constructors example.cpp -o example_asand then execute. No sanitizer errors indicate the moved‑from object is not accessed incorrectly. - Inspect the generated assembly (e.g.,
objdump -d example) and look for simple pointer moves (mov) rather than loops ormemcpycalls when optimizations (-O2) are enabled.
Actionable takeaway
If you manage large resources—containers, GPU buffers, file handles, or custom allocators—implement move constructors and assignment operators (or rely on the compiler‑generated ones for trivially movable types). Use std::move explicitly when you want to transfer ownership, and always treat a moved‑from object as needing re‑initialization before further use. Verify your implementation with the steps above to ensure you gain the zero‑copy performance benefit without introducing subtle bugs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.