Move Semantics in C++
[!NOTE] You can reference the following videos:
Beginning with C++11, the C++ language introduced move semantics, a mechanism that enables the efficient transfer of resources from one object to another. Move semantics were added to reduce unnecessary copying of heavy objects such as containers, strings, and objects managing dynamic memory or other resources (e.g., file handles).
We provide a detailed and rigorous explanation of move semantics, starting from basic reference types and progressing to move constructors, move assignment operators, and appropriate usage patterns.
2. Value Categories and Reference Types
Understanding move semantics requires understanding how the C++ type system distinguishes between objects that are persistent and objects that are temporary. These distinctions are expressed using value categories and reference types.
2.1 Lvalues
An lvalue is an expression that refers to a persistent object with a name or a stable address. Examples:
int x = 10; // x is an lvalue
Lvalues can be bound to lvalue references of the form:
T&
2.2 Rvalues
An rvalue is an expression that represents a temporary value that cannot be taken as a stable location. Examples:
10 // rvalue literal
x + 5 // rvalue expression
std::string("hi") // temporary
Rvalues can be bound to rvalue references of the form:
T&&
2.3 Why This Distinction Matters
The fundamental insight is that temporaries may be safely βmoved fromβ, because they will soon be destroyed. This allows resource transfer rather than duplication.
3. Move Constructors and Move Assignment
C++ classes that manage resources should be designed to support efficient movement. This requires defining the following two special member functions:
- Move Constructor: transfers resources from a source object into a destination object.
- Move Assignment Operator: transfers resources during assignment.
Both functions typically take an argument of type T&&.
3.1 Example Class
Consider a simplified string-like class:
class MyString {
private:
char* data;
public:
// Constructor
MyString(const char* s) {
size_t n = std::strlen(s) + 1;
data = new char[n];
std::memcpy(data, s, n);
}
// Destructor
~MyString() {
delete[] data;
}
// Copy constructor (deep copy)
MyString(const MyString& other) {
size_t n = std::strlen(other.data) + 1;
data = new char[n];
std::memcpy(data, other.data, n);
}
3.2 Move Constructor
A move constructor steals the resource pointer and leaves the source object in a valid but empty state:
MyString(MyString&& other) noexcept
: data(other.data)
{
other.data = nullptr;
}
3.3 Move Assignment Operator
Move assignment must release the current resources, then take ownership of the source resources:
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
3.4 Post-conditions of Move
- The destination object now owns the resource previously held by the source.
- The source object remains valid but unspecified β operations like destruction are safe.
4. std::move and Enabling Moves
4.1 Purpose of std::move
std::move is a standard utility function that converts its argument into an rvalue reference.
It does not move anything by itself.
It is equivalent to:
static_cast<T&&>(obj)
4.2 Why std::move Is Necessary
Named objects are always lvalues, even if they represent temporary data. Therefore, to call a move constructor or move assignment operator on a named object, you must explicitly convert it into an rvalue reference.
Example:
MyString a("Hello");
MyString b = std::move(a); // Invokes move constructor
Without std::move, the copy constructor would be called instead.
5. When Moves Occur Automatically
Moves may happen implicitly:
5.1 Returning Local Objects
MyString f() {
MyString temp("abc");
return temp; // eligible for move (or copy elision)
}
Compilers may:
- optimize the copy entirely (Return Value Optimization), or
- perform a move instead of a copy if RVO does not apply.
5.2 Insertion into Containers
std::vector<MyString> v;
v.push_back(MyString("Hello")); // moves temporary
Temporary objects passed by value are perfect candidates for moves.
6. When to Implement Move Semantics
Move semantics should be provided for any class that:
- Manages dynamic memory
- Owns file handles
- Owns sockets or network resources
- Uses large buffers or other heavy structures
If your class does not manage any resources, move semantics may provide little or no benefit.
7. Common Pitfalls
7.1 Using a Moved-From Object Incorrectly
After an object has been moved from, it is only safe to:
- destroy it,
- reassign it,
- pass it again to a function that expects a moved-from object.
Using its data members is undefined behavior unless you explicitly define a safe moved-from state.
7.2 Forgetting noexcept
Move constructors that are not marked noexcept may cause standard library containers to fall back to copying instead of moving.
Best practice:
MyClass(MyClass&& other) noexcept { ... }
8. Summary
- C++ distinguishes between lvalues and rvalues to enable efficient resource transfer.
- Rvalue references (
T&&) bind to temporary objects and allow moving rather than copying. - Move constructors and move assignment operators transfer ownership instead of duplicating resources.
std::moveis a cast that enables movement from named objects.- Move semantics reduce allocations and improve performance in modern C++ programs.