Move Semantics & Rvalue References in C++
Move semantics is a C++11 feature that enables transferring ownership of a resource from one object to another without copying the data. This is very important for performance because it avoids expensive memory allocation and data copying, especially for large objects such as strings, vectors, or objects that manage resources.
To understand move semantics, we need to understand the difference between lvalues and rvalues:
- Lvalue — An object that has a name and an identity (a fixed memory address). It can be on the left side of an assignment. Example:
int x = 10;→xis an lvalue. - Rvalue — A temporary object that has no name and usually exists only briefly. It can be on the right side of an assignment. Examples:
10,x + 1,string("Hello")→ all rvalues.
The && operator (rvalue reference) is the key to move semantics. It allows us to detect when an object is a temporary (an rvalue) and "move" its resource instead of copying it. std::move explicitly converts an lvalue into an rvalue reference, telling the compiler that we want to move that object.
std::forward is used in perfect forwarding to preserve the value category (lvalue/rvalue) when forwarding arguments to another function. This is important in templates to ensure correct behavior.
emplace_back vs push_back: emplace_back uses forwarding to construct the object in place directly, avoiding the creation of a temporary object and any move/copy.
Code Example
#include <iostream>
#include <string>
#include <vector>
#include <utility>
using namespace std;
// ==================== Basic Lvalue and Rvalue ====================
void basicLvalueRvalue() {
cout << "=== Basic Lvalue and Rvalue ===" << endl;
int x = 10; // x is an lvalue, 10 is an rvalue
int y = x + 5; // y is an lvalue, (x + 5) is an rvalue
string s1 = "Hi"; // s1 is an lvalue, "Hi" is a temporary rvalue
// Lvalue reference
int& ref = x; // ref refers to x (an lvalue)
ref = 20;
cout << "x = " << x << endl; // x = 20
// Rvalue reference
int&& rref = 100; // rref refers to the rvalue 100
cout << "rref = " << rref << endl;
// std::move converts an lvalue into an rvalue reference
int z = 42;
int&& moved = move(z);
cout << "z = " << z << ", moved = " << moved << endl;
// z still holds 42, but it can now be "moved"
}
// ==================== Move Constructor ====================
class MyString {
private:
char* data;
int length;
public:
// Regular constructor (deep copy)
MyString(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
cout << "Constructor: \"" << data << "\" (length=" << length << ")" << endl;
}
// Copy constructor (deep copy - expensive!)
MyString(const MyString& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
cout << "Copy constructor: \"" << data << "\"" << endl;
}
// Move constructor (transfer ownership - cheap!)
MyString(MyString&& other) noexcept {
data = other.data; // Take the pointer
length = other.length; // Take the length
// Set the source to a safe state
other.data = nullptr;
other.length = 0;
cout << "Move constructor: \"" << data << "\"" << endl;
}
// Copy assignment operator
MyString& operator=(const MyString& other) {
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
cout << "Copy assignment: \"" << data << "\"" << endl;
}
return *this;
}
// Move assignment operator
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data; // Free the old resource
data = other.data; // Transfer ownership
length = other.length;
other.data = nullptr;
other.length = 0;
cout << "Move assignment: \"" << data << "\"" << endl;
}
return *this;
}
~MyString() {
cout << "Destructor: \"" << (data ? data : "nullptr") << "\"" << endl;
delete[] data;
}
void display() const {
cout << "String: \"" << (data ? data : "nullptr") << "\" (length=" << length << ")" << endl;
}
};
// ==================== Perfect Forwarding ====================
template <typename T>
void wrapper(T&& arg) {
// std::forward preserves the value category
cout << "wrapper: forwarding " << arg << endl;
}
class Processor {
public:
void process(string& s) {
cout << "Process lvalue: " << s << endl;
}
void process(string&& s) {
cout << "Process rvalue: " << s << endl;
}
};
template <typename T>
void perfectForward(Processor& proc, T&& value) {
// std::forward preserves whether value is an lvalue or an rvalue
proc.process(forward<T>(value));
}
// ==================== emplace_back vs push_back ====================
void emplaceVsPush() {
cout << "\n=== emplace_back vs push_back ===" << endl;
vector<MyString> vec;
cout << "--- push_back ---" << endl;
MyString temp("Hello");
vec.push_back(temp); // Copy or move temp
cout << "\n--- emplace_back ---" << endl;
vec.emplace_back("World"); // Construct in place directly
// No temporary object needed, no copy/move needed
cout << "\n--- push_back with an rvalue ---" << endl;
vec.push_back(MyString("Move me")); // Move (temporary object)
cout << "\n--- emplace_back directly ---" << endl;
vec.emplace_back("Direct"); // Direct construction
}
// ==================== Move Optimization ====================
vector<int> createLargeVector(int n) {
vector<int> v(n);
for (int i = 0; i < n; i++) {
v[i] = i;
}
return v; // NRVO (Named Return Value Optimization) or a move
}
int main() {
basicLvalueRvalue();
// ==================== Move Semantics Demo ====================
cout << "\n=== Move Semantics Demo ===" << endl;
MyString s1("First");
cout << "\n--- Copy ---" << endl;
MyString s2 = s1; // Copy constructor
s1.display();
s2.display();
cout << "\n--- Move ---" << endl;
MyString s3 = move(s1); // Move constructor
s1.display(); // nullptr after the move
s3.display();
cout << "\n--- Move Assignment ---" << endl;
MyString s4("Fourth");
s4 = move(s3); // Move assignment
s3.display(); // nullptr after the move
s4.display();
// ==================== Perfect Forwarding ====================
cout << "\n=== Perfect Forwarding ===" << endl;
Processor proc;
string name = "John";
perfectForward(proc, name); // Forward as an lvalue
perfectForward(proc, string("Peter")); // Forward as an rvalue
// ==================== emplace_back vs push_back ====================
emplaceVsPush();
// ==================== Performance Example ====================
cout << "\n=== Performance Example ===" << endl;
{
vector<MyString> performance;
cout << "push_back temporary:" << endl;
performance.push_back(MyString("Test"));
cout << "\nemplace_back directly:" << endl;
performance.emplace_back("Test");
}
// ==================== Move with a vector ====================
cout << "\n=== Move with Vector ===" << endl;
{
vector<int> v1 = createLargeVector(5);
cout << "v1 size: " << v1.size() << endl;
// Move v1 into v2
vector<int> v2 = move(v1);
cout << "v2 size: " << v2.size() << endl;
cout << "v1 size after move: " << v1.size() << endl;
}
cout << "\n=== Best Practices ===" << endl;
cout << "1. Use std::move() to move lvalues that are no longer used." << endl;
cout << "2. Write a move constructor and a move assignment operator for classes that manage resources." << endl;
cout << "3. Mark move operations as noexcept for container optimization." << endl;
cout << "4. Use emplace_back() instead of push_back() to avoid temporary objects." << endl;
cout << "5. Use perfect forwarding for template functions that forward arguments." << endl;
return 0;
}
Code Explanation
-
int&& rref = 100— An rvalue reference referring to the temporary value 100. This is the foundation of move semantics. -
std::move(z)— Convertsz(an lvalue) into an rvalue reference. It does not actually "move" anything; it only changes the value's category. -
MyString(MyString&& other) noexcept— A move constructor. Takes the resource fromotherwithout copying. After the move,otheris in a safe state (nullptr). -
data = other.data— In the move constructor, we only copy the pointer (very cheap) instead of copying the entire character array (very expensive). -
noexcept— Indicates that the move constructor/assignment does not throw an exception. Important for std::vector, which uses moves when reallocating. -
operator=(MyString&& other)— A move assignment operator. Same as the move constructor but for assignment. It must handle self-assignment and clean up the old resource. -
template <typename T> void wrapper(T&& arg)— A universal reference (not a regular rvalue reference).T&&in a template can refer to either an lvalue or an rvalue depending on the argument. -
std::forward<T>(value)— Perfect forwarding. Preserves the value category (lvalue/rvalue) when forwarding an argument to another function. -
vec.emplace_back("World")— Constructs the object directly inside the vector. More efficient thanpush_back(MyString("World"))because it avoids a temporary object.
Everyday Use Cases
Imagine you are building an image processing application. Each image is large (hundreds of MB). Without move semantics, copying an image into a function or container would be very slow because it would have to reallocate memory and copy all the data.
class Image {
unsigned char* pixels;
int width, height;
public:
// Move constructor - just move the pointer, don't copy the pixels!
Image(Image&& other) noexcept
: pixels(other.pixels), width(other.width), height(other.height) {
other.pixels = nullptr;
}
};
vector<Image> gallery;
gallery.push_back(Image("photo.jpg")); // Move, not copy!
// Even better with emplace_back
gallery.emplace_back("photo.jpg"); // Direct construction in place
Conclusion
Move semantics enables efficient resource transfer without copying data. Rvalue references (&&) detect temporary objects that can be moved. std::move converts an lvalue into an rvalue, and std::forward preserves the value category for perfect forwarding. emplace_back is more efficient than push_back because it constructs the object directly in place. Always write a move constructor/assignment for classes that manage dynamic resources.