Copying objects is expensive. Whether it’s a large std::vector
, a buffer, or a file handle — copying means duplicating memory, and that costs performance.
Move semantics, introduced in C++11, is a powerful solution that allows us to transfer ownership of resources rather than duplicating them. It enables efficient memory and resource management — especially for temporary objects.
What are Move Semantics?
Move semantics allow an object to steal the internal resources (e.g., dynamic memory, file handles) of another object rather than copying them.
It involves:
Move constructor
Move assignment operator
Both are designed to transfer data from one object to another, leaving the source in a valid but empty state.
Why Do We Need Move Semantics?
❌ The Problem with Copying
Suppose you have a class that holds a dynamically allocated array:
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s) {
data = new int[size];
}
~Buffer() {
delete[] data;
}
Buffer(const Buffer& other); // Copy constructor
Buffer& operator=(const Buffer& other); // Copy assignment
};
Copying a Buffer
involves deep copying the internal array — expensive for large data.
✅ Enter Move Semantics
With move semantics, you can write:
Buffer(Buffer&& other); // Move constructor
Buffer& operator=(Buffer&& other); // Move assignment
This allows you to transfer ownership of data
from one object to another without copying the contents.
Copy vs Move: Key Differences
Feature | Copy | Move |
---|---|---|
Purpose | Duplicate resource | Transfer resource ownership |
Performance | Slow (deep copy) | Fast (pointer swap, no copy) |
Used when | Lvalues (named variables) | Rvalues (temporaries, std::move ) |
A Minimal Example
Step 1: A Class With Move Semantics
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s) {
data = new int[size];
std::cout << "Constructed\n";
}
~Buffer() {
delete[] data;
std::cout << "Destroyed\n";
}
// Copy constructor
Buffer(const Buffer& other) : size(other.size) {
data = new int[size];
std::copy(other.data, other.data + size, data);
std::cout << "Copied\n";
}
// Move constructor
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
std::cout << "Moved\n";
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // clean up current resource
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
std::cout << "Move Assigned\n";
}
return *this;
}
};
Step 2: Usage Example
Buffer makeBuffer() {
Buffer temp(1000);
return temp; // Move constructor is called here
}
int main() {
Buffer b1 = makeBuffer(); // Moved from temp
Buffer b2 = std::move(b1); // Explicit move
}
When to Use std::move
Use std::move(obj)
when you:
Want to explicitly transfer ownership
Know that
obj
won’t be used again in its original form
Example:
Buffer a(1000);
Buffer b = std::move(a); // a is now in an empty/valid state
⚠️ After std::move(a)
, the original a
should not be used (except maybe to destroy or reset).
STL & Move Semantics
The Standard Template Library (STL) uses move semantics heavily:
std::vector
will move elements instead of copying them when resizingstd::unique_ptr
is non-copyable, but movableemplace_back()
uses move semantics by default
This makes your code:
Faster
Safer
Cleaner
The Role of std::move
std::move is an utility function that facilitates the use of move semantics. It is defined in the <utility>
header and is a key component in implementing efficient resource transfers. The primary purpose of std::move
is to cast an object tot an rvalue reference, indicating that it can moved from.
Basic Syntax:
The syntax of std::move
is simple:
#include <utility>
T&& std::move(T&& arg);
Here, T
represents the type of the object, and arg
is the object to be cast to an rvalue reference. The result of std::move
is an rvalue reference of type T
.
How std::move Works
When you apply std::move
to an object, you are essentially telling the compiler that you are done with the object, and it can safely “move” the resources it owns. This is particularly useful in scenarios involving dynamic memory allocation or resource management.
Consider the following example:
#include <iostream>
#include <vector>
int main() {
std::vector<int> source = {1, 2, 3, 4, 5};
std::vector<int> destination = std::move(source);
std::cout << "Source size: " << source.size() << std::endl; // Undefined behavior
std::cout << "Destination size: " << destination.size() << std::endl; // Outputs 5
return 0;
}
// Output
Source size: 0
Destination size: 5
In this example, std::move
is used to transfer the contents of the source
vector to the destination
vector. After the move, source
is left in a valid but unspecified state, and accessing its size is undefined behavior.