C++ Move Semantics
Introduction
In modern C++ programming, efficiently managing memory is crucial for writing high-performance applications. Move semantics, introduced in C++11, represents one of the most significant enhancements to the language, providing a mechanism to avoid unnecessary copying of resources when objects are transferred.
Before move semantics, when you passed an object to a function or returned one from a function, C++ would typically create a copy of that object. This copying operation could be expensive, especially for objects that manage substantial resources like large arrays, strings, or complex data structures.
In this tutorial, you'll learn:
- What move semantics is and why it matters
- How to implement move constructors and move assignment operators
- When and how to use
std::move
- Practical applications and best practices
Understanding Move Semantics
The Problem: Expensive Copies
Let's first understand why copies can be expensive:
#include <iostream>
#include <vector>
void processVector(std::vector<int> vec) {
// Do something with vec
std::cout << "Vector size: " << vec.size() << std::endl;
}
int main() {
std::vector<int> myVector(1000000, 1); // A large vector
// This will make a copy of the entire vector
processVector(myVector);
return 0;
}
In this example, when myVector
is passed to processVector()
, a copy of the vector containing 1 million integers is created. This involves:
- Allocating new memory for the copy
- Copying each element from the original to the new allocation
- Deallocating the memory when the function completes
This is inefficient, especially when the function doesn't need to modify the original object.
The Solution: Moving Instead of Copying
Move semantics enables the transfer of resources from one object to another without making a deep copy. The source object is left in a valid but unspecified state after the move operation.
Let's see how we can improve our previous example:
#include <iostream>
#include <vector>
#include <utility> // For std::move
void processVector(std::vector<int> vec) {
// Do something with vec
std::cout << "Vector size: " << vec.size() << std::endl;
}
int main() {
std::vector<int> myVector(1000000, 1); // A large vector
// This will move the vector rather than copy it
processVector(std::move(myVector));
// myVector is now in a valid but unspecified state
std::cout << "myVector size after move: " << myVector.size() << std::endl;
return 0;
}
Output:
Vector size: 1000000
myVector size after move: 0
In this improved version, std::move
converts myVector
into an rvalue, which tells the compiler to use move semantics. The vector's internal resources (the dynamically allocated array) are simply transferred to the function parameter, avoiding the expensive copying operation.
R-Value References
To support move semantics, C++11 introduced r-value references, denoted by double ampersands (&&
). An r-value reference binds to temporary objects or objects that are about to be destroyed (r-values).
Understanding L-values and R-values
- L-value: An expression that refers to a memory location and can appear on the left side of an assignment.
- R-value: A temporary expression that doesn't have a persistent memory address and typically appears on the right side of an assignment.
int x = 5; // 'x' is an l-value, '5' is an r-value
int y = x; // both 'x' and 'y' are l-values
int z = x + y; // 'x + y' is an r-value
R-value References in Action
R-value references allow us to create functions that specifically handle temporary objects:
#include <iostream>
#include <string>
// Function taking an l-value reference
void process(std::string& str) {
std::cout << "Called with l-value: " << str << std::endl;
}
// Function taking an r-value reference
void process(std::string&& str) {
std::cout << "Called with r-value: " << str << std::endl;
}
int main() {
std::string stable = "Hello";
process(stable); // Calls the l-value version
process(std::string("Temporary")); // Calls the r-value version
process(std::move(stable)); // Calls the r-value version
return 0;
}
Output:
Called with l-value: Hello
Called with r-value: Temporary
Called with r-value: Hello
Notice that std::move(stable)
converts stable
to an r-value reference, causing the r-value version of process()
to be called.
Implementing Move Operations
To take advantage of move semantics in your own classes, you need to implement:
- A move constructor
- A move assignment operator
Move Constructor
The move constructor initializes an object by transferring resources from a temporary object:
MyClass(MyClass&& other) noexcept {
// Transfer resources from other to this
// Leave other in a valid but unspecified state
}
Move Assignment Operator
The move assignment operator transfers resources from a temporary object to an existing object:
MyClass& operator=(MyClass&& other) noexcept {
// Release any resources currently held by this
// Transfer resources from other to this
// Leave other in a valid but unspecified state
return *this;
}
Complete Example: A Simple Resource-Managing Class
Let's create a class that manages a dynamic array and implements move semantics:
#include <iostream>
#include <utility>
class DynamicArray {
private:
int* data;
size_t size;
public:
// Constructor
DynamicArray(size_t size) : size(size) {
std::cout << "Constructor called" << std::endl;
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = 0;
}
}
// Destructor
~DynamicArray() {
std::cout << "Destructor called" << std::endl;
delete[] data;
}
// Copy constructor
DynamicArray(const DynamicArray& other) : size(other.size) {
std::cout << "Copy constructor called" << std::endl;
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i];
}
}
// Copy assignment operator
DynamicArray& operator=(const DynamicArray& other) {
std::cout << "Copy assignment operator called" << std::endl;
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i];
}
}
return *this;
}
// Move constructor
DynamicArray(DynamicArray&& other) noexcept : data(other.data), size(other.size) {
std::cout << "Move constructor called" << std::endl;
other.data = nullptr;
other.size = 0;
}
// Move assignment operator
DynamicArray& operator=(DynamicArray&& other) noexcept {
std::cout << "Move assignment operator called" << std::endl;
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
// Get the size
size_t getSize() const {
return size;
}
// Set a value
void setValue(size_t index, int value) {
if (index < size) {
data[index] = value;
}
}
// Get a value
int getValue(size_t index) const {
if (index < size) {
return data[index];
}
return -1;
}
};
DynamicArray createArray(size_t size) {
return DynamicArray(size);
}
int main() {
// Using copy semantics
std::cout << "Creating array1..." << std::endl;
DynamicArray array1(5);
array1.setValue(0, 10);
std::cout << "\nCopying array1 to array2..." << std::endl;
DynamicArray array2 = array1;
std::cout << "\nArray2[0] = " << array2.getValue(0) << std::endl;
// Using move semantics
std::cout << "\nMoving temporary array to array3..." << std::endl;
DynamicArray array3 = createArray(10);
std::cout << "\nMoving array2 to array4..." << std::endl;
DynamicArray array4 = std::move(array2);
std::cout << "\nArray4 size: " << array4.getSize() << std::endl;
std::cout << "Array2 size after move: " << array2.getSize() << std::endl;
return 0;
}
Output:
Creating array1...
Constructor called
Copying array1 to array2...
Copy constructor called
Array2[0] = 10
Moving temporary array to array3...
Constructor called
Move constructor called
Destructor called
Moving array2 to array4...
Move constructor called
Array4 size: 5
Array2 size after move: 0
Destructor called
Destructor called
Destructor called
Destructor called
Let's analyze the output:
- When
array1
is created, the regular constructor is called. - When
array2
is created and initialized fromarray1
, the copy constructor is called. - When
array3
is initialized from the temporary object returned bycreateArray()
, the temporary object is moved rather than copied. - When
array4
is initialized fromstd::move(array2)
, the move constructor is called, andarray2
's size becomes 0 after the move.
The std::move
Function
std::move
doesn't actually move anything—it's a cast that converts an lvalue into an rvalue reference, signaling to the compiler that the object can be "moved from".
When to Use std::move
-
When you no longer need the source object's value:
cppstd::vector<int> source = {1, 2, 3};
std::vector<int> destination = std::move(source);
// source is now in a valid but unspecified state -
When returning a local object from a function:
cppstd::vector<int> createVector() {
std::vector<int> result = {1, 2, 3};
// No need for std::move here, as return value optimization (RVO)
// will likely eliminate the copy/move entirely
return result;
}However, in more complex cases where RVO might not apply:
cppstd::vector<int> createConditionalVector(bool condition) {
std::vector<int> result1 = {1, 2, 3};
std::vector<int> result2 = {4, 5, 6};
// RVO might not work here due to the condition
return condition ? std::move(result1) : std::move(result2);
} -
When passing an argument to a function that accepts an rvalue reference:
cppvoid processVector(std::vector<int>&& vec) {
// Process the vector
}
std::vector<int> myVector = {1, 2, 3};
processVector(std::move(myVector));
Common Mistakes with std::move
-
Moving objects that you still need to use:
cppstd::string name = "John";
std::string greeting = "Hello, " + std::move(name) + "!";
std::cout << name << std::endl; // Undefined behavior or empty string -
Moving const objects:
cppconst std::vector<int> constVec = {1, 2, 3};
std::vector<int> newVec = std::move(constVec); // Still performs a copyMoving a const object still results in a copy because the const qualification prevents modifying the source object, which is necessary for a true move operation.
Real-World Applications
1. Implementing a Customized String Class
#include <iostream>
#include <cstring>
#include <utility>
class MyString {
private:
char* data;
size_t length;
public:
// Default constructor
MyString() : data(nullptr), length(0) {
std::cout << "Default constructor" << std::endl;
}
// Constructor from C-string
MyString(const char* str) {
std::cout << "Constructor from C-string" << std::endl;
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// Copy constructor
MyString(const MyString& other) : length(other.length) {
std::cout << "Copy constructor" << std::endl;
data = new char[length + 1];
strcpy(data, other.data);
}
// Move constructor
MyString(MyString&& other) noexcept : data(other.data), length(other.length) {
std::cout << "Move constructor" << std::endl;
other.data = nullptr;
other.length = 0;
}
// Copy assignment operator
MyString& operator=(const MyString& other) {
std::cout << "Copy assignment operator" << std::endl;
if (this != &other) {
delete[] data;
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
return *this;
}
// Move assignment operator
MyString& operator=(MyString&& other) noexcept {
std::cout << "Move assignment operator" << std::endl;
if (this != &other) {
delete[] data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
return *this;
}
// Destructor
~MyString() {
std::cout << "Destructor" << std::endl;
delete[] data;
}
// Concatenation operator
MyString operator+(const MyString& other) const {
MyString result;
result.length = length + other.length;
result.data = new char[result.length + 1];
strcpy(result.data, data);
strcat(result.data, other.data);
return result;
}
// Print the string
void print() const {
if (data) {
std::cout << "String: " << data << ", Length: " << length << std::endl;
} else {
std::cout << "Empty string" << std::endl;
}
}
};
int main() {
// Creating strings
MyString s1("Hello");
s1.print();
// Copy constructor
std::cout << "\nCopying s1 to s2:" << std::endl;
MyString s2 = s1;
s2.print();
// Move constructor
std::cout << "\nMoving temporary to s3:" << std::endl;
MyString s3 = MyString("World");
s3.print();
// Move assignment
std::cout << "\nMoving s2 to s4:" << std::endl;
MyString s4;
s4 = std::move(s2);
s4.print();
s2.print(); // s2 is now empty
// Concatenation with move
std::cout << "\nConcatenating s1 and s3 to s5:" << std::endl;
MyString s5 = s1 + s3;
s5.print();
return 0;
}
Output:
Constructor from C-string
String: Hello, Length: 5
Copying s1 to s2:
Copy constructor
String: Hello, Length: 5
Moving temporary to s3:
Constructor from C-string
Move constructor
Destructor
String: World, Length: 5
Moving s2 to s4:
Default constructor
Move assignment operator
String: Hello, Length: 5
Empty string
Concatenating s1 and s3 to s5:
Default constructor
String: HelloWorld, Length: 10
Destructor
Destructor
Destructor
Destructor
Destructor
2. Implementing a Smart Resource Handle
This example demonstrates using move semantics to create a simple unique resource handler, similar to std::unique_ptr
:
#include <iostream>
#include <utility>
// A simple resource class
class Resource {
public:
Resource() {
std::cout << "Resource acquired" << std::endl;
}
~Resource() {
std::cout << "Resource released" << std::endl;
}
void use() const {
std::cout << "Resource used" << std::endl;
}
};
// A unique resource handle
template <typename T>
class UniqueHandle {
private:
T* resource;
public:
// Constructor
explicit UniqueHandle(T* res = nullptr) : resource(res) {}
// Destructor
~UniqueHandle() {
delete resource;
}
// Move constructor
UniqueHandle(UniqueHandle&& other) noexcept : resource(other.resource) {
other.resource = nullptr;
}
// Move assignment operator
UniqueHandle& operator=(UniqueHandle&& other) noexcept {
if (this != &other) {
delete resource;
resource = other.resource;
other.resource = nullptr;
}
return *this;
}
// Disable copying
UniqueHandle(const UniqueHandle&) = delete;
UniqueHandle& operator=(const UniqueHandle&) = delete;
// Access the resource
T* get() const {
return resource;
}
// Dereference operators
T& operator*() const {
return *resource;
}
T* operator->() const {
return resource;
}
// Release ownership
T* release() {
T* temp = resource;
resource = nullptr;
return temp;
}
// Reset with a new resource
void reset(T* res = nullptr) {
delete resource;
resource = res;
}
};
// Function that returns a UniqueHandle
UniqueHandle<Resource> createResource() {
return UniqueHandle<Resource>(new Resource());
}
int main() {
// Create a resource and handle
std::cout << "Creating first handle" << std::endl;
UniqueHandle<Resource> handle1(new Resource());
// Use the resource
std::cout << "\nUsing first resource" << std::endl;
handle1->use();
// Move the handle
std::cout << "\nMoving first handle to second handle" << std::endl;
UniqueHandle<Resource> handle2 = std::move(handle1);
// First handle is now empty, second has the resource
if (handle1.get() == nullptr) {
std::cout << "First handle is now empty" << std::endl;
}
// Use the resource through the second handle
std::cout << "\nUsing second resource" << std::endl;
handle2->use();
// Get a handle from a function
std::cout << "\nGetting a handle from a function" << std::endl;
UniqueHandle<Resource> handle3 = createResource();
handle3->use();
std::cout << "\nExiting program" << std::endl;
return 0;
}
Output:
Creating first handle
Resource acquired
Using first resource
Resource used
Moving first handle to second handle
First handle is now empty
Using second resource
Resource used
Getting a handle from a function
Resource acquired
Resource used
Exiting program
Resource released
Resource released
In this example, the UniqueHandle
class demonstrates how move semantics allows for the safe transfer of ownership of a resource, ensuring it's properly cleaned up exactly once when the last handle goes out of scope.
Performance Benefits of Move Semantics
To truly appreciate move semantics, let's benchmark the performance difference between copying and moving large objects:
#include <iostream>
#include <vector>
#include <chrono>
#include <string>
// Function to measure elapsed time
template<typename Func>
long long measureTime(Func func) {
auto start = std::chrono::high_resolution_clock::now();
func();
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
}
int main() {
const int NUM_ITERATIONS = 1000;
const int VECTOR_SIZE = 100000;
// Create a large vector
std::vector<int> largeVector(VECTOR_SIZE);
for (int i = 0; i < VECTOR_SIZE; ++i) {
largeVector[i] = i;
}
// Measure time for copying
long long copyTime = measureTime([&]() {
for (int i = 0; i < NUM_ITERATIONS; ++i) {
std::vector<int> copy = largeVector;
// Do something with copy to prevent optimization
copy[0] = i;
}
});
// Measure time for moving
long long moveTime = measureTime([&]() {
for (int i = 0; i < NUM_ITERATIONS; ++i) {
std::vector<int> original = largeVector;
std::vector<int> moved = std::move(original);
// Do something with moved to prevent optimization
moved[0] = i;
}
});
std::cout << "Time for " << NUM_ITERATIONS << " copies: " << copyTime << " microseconds" << std::endl;
std::cout << "Time for " << NUM_ITERATIONS << " moves: " << moveTime << " microseconds" << std::endl;
std::cout << "Move is " << static_cast<double>(copyTime) / moveTime << " times faster" << std::endl;
return 0;
}
Example Output:
Time for 1000 copies: 203458 microseconds
Time for 1000 moves: 32019 microseconds
Move is 6.35 times faster
The actual numbers will vary depending on your system, but you should consistently see that moving is significantly faster than copying for large objects.
Move Semantics and the Rule of Five
In C++, there's the "Rule of Three" which states that if you define any of a destructor, copy constructor, or copy assignment operator, you should define all three. With the introduction of move semantics, this expanded to the "Rule of Five":
If your class manages resources, you should define or explicitly disable:
- Destructor
- Copy constructor
- Copy assignment operator
- Move constructor
- Move assignment operator
class RuleOfFiveExample {
private:
int* data;
public:
// Constructor
RuleOfFiveExample(int value) : data(new int(value)) {}
// 1. Destructor
~RuleOfFiveExample() {
delete data;
}
// 2. Copy constructor
RuleOfFiveExample(const RuleOfFiveExample& other) : data(new int(*other.data)) {}
// 3. Copy assignment operator
RuleOfFiveExample& operator=(const RuleOfFiveExample& other) {
if (this != &other) {
*data = *other.data;
}
return *this;
}
// 4. Move constructor
RuleOfFiveExample(RuleOfFiveExample&& other) noexcept : data(other.data) {
other.data = nullptr;
}
// 5. Move assignment operator
RuleOfFiveExample& operator=(RuleOfFiveExample&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
Move Semantics with Standard Library Containers
Standard library containers like std::vector
, std::string
, std::unique_ptr
, etc., all support move semantics. This makes operations like inserting elements into containers much more efficient:
#include <iostream>
#include <vector>
#include <string>
#include <utility>
int main() {
std::vector<std::string> names;
// Push back with copy
std::string name = "John Doe";
names.push_back(name);
std::cout << "After copy, name = " << name << std::endl;
// Push back with move
names.push_back(std::move(name));
std::cout << "After move, name = " << name << std::endl;
// Emplace back constructs in place
names.emplace_back("Jane Doe");
// Print all names
std::cout << "\nNames in vector:" << std::endl;
for (const auto& n : names) {
std::cout << " " << n << std::endl;
}
return 0;
}
Output:
After copy, name = John Doe
After move, name =
Names in vector:
John Doe
John Doe
Jane Doe
Notice how name
becomes empty after being moved. Also, note the use of emplace_back
, which constructs the object in place, avoiding both copying and moving.
Summary
Move semantics is a powerful feature in C++ that allows for significant performance improvements by avoiding unnecessary copying of resources. It's especially valuable when working with objects that manage large amounts of memory or other expensive resources.
Key takeaways:
- Move semantics transfers resources instead of copying them
- Use r-value references (
Type&&
) to enable move operations - Implement move constructors and move assignment operators for your own classes
- Use
std::move
to convert l-values to r-values, enabling move operations - Follow the Rule of Five for classes that manage resources
- Standard library containers support move semantics for better performance
By mastering move semantics, you can write more efficient C++ code that minimizes unnecessary copying and resource duplication.
Additional Resources
- C++ Reference: Move Constructors
- C++ Reference: Move Assignment
- C++ Reference: std::move
- Book: "Effective Modern C++" by Scott Meyers (especially Items 23-30)
Exercises
- Implement a simplified
std::vector
-like class that manages a dynamic array and supports move semantics. - Modify the
MyString
class to add aappend
method that efficiently concatenates another string. - Create a class representing a database connection that cannot be copied but can be moved.
- Write a function that takes a vector of strings and returns a new vector containing only the strings that are longer than a given length. Use move semantics to avoid copying the strings.
- Benchmark the performance difference between copying and moving for a custom class that contains a large vector, a map, and several strings.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)