Imagine you're moving houses. The old way (copy semantics) is like buying duplicates of all your furniture for the new house, then throwing away the originals. Move semantics is like taking your actual furniture to the new house - much more efficient!
class String {
private:
char* data;
size_t length;
public:
// Copy constructor - expensive for large strings!
String(const String& other)
: length(other.length) {
data = new char[length + 1]; // Allocate new memory
std::strcpy(data, other.data); // Copy all characters
}
// Copy assignment - also expensive!
String& operator=(const String& other) {
if (this != &other) {
delete[] data; // Clean up old data
length = other.length;
data = new char[length + 1]; // Allocate new memory
std::strcpy(data, other.data); // Copy all characters
}
return *this;
}
};
Before diving into move semantics, we need to understand lvalues and rvalues. Think of lvalues as things with addresses (like houses) and rvalues as temporary things (like the pizza delivery guy).
int x = 42; // x is lvalue, 42 is rvalue
int y = x; // y is lvalue, x is lvalue (lvalue in rvalue context)
int& lr = x; // OK: lvalue reference to lvalue
int& lr2 = 42; // ERROR: cannot bind lvalue reference to rvalue
const int& cr = 42; // OK: const lvalue reference can bind to rvalue
int&& rr = 42; // OK: rvalue reference to rvalue
int&& rr2 = x; // ERROR: cannot bind rvalue reference to lvalue
// Function returning by value creates an rvalue
std::string getName() { return "temporary"; }
std::string s = getName(); // getName() returns an rvalue
// But you can't do:
// getName() = "something"; // ERROR: can't assign to rvalue
Rvalue references (&&) are like temporary ownership papers. They let you take ownership of resources that are about to be destroyed anyway.
Now we can implement efficient move operations that transfer ownership instead of copying data.
class String {
private:
char* data;
size_t length;
public:
// Move constructor - takes ownership of temporary
String(String&& other) noexcept
: data(other.data), length(other.length) {
// "Steal" the resources
other.data = nullptr; // Leave other in valid but empty state
other.length = 0;
}
// Move assignment operator
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data; // Clean up current resources
// "Steal" the resources
data = other.data;
length = other.length;
// Leave other in valid but empty state
other.data = nullptr;
other.length = 0;
}
return *this;
}
// Regular constructors and destructor
String(const char* str) {
length = std::strlen(str);
data = new char[length + 1];
std::strcpy(data, str);
}
~String() {
delete[] data;
}
};
std::move doesn't actually move anything! It's just a cast that tells the compiler "treat this lvalue as an rvalue". It's like putting a "free to take" sign on your furniture.
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = v1; // Copy: v1 still has data
std::vector<int> v3 = {1, 2, 3, 4, 5};
std::vector<int> v4 = std::move(v3); // Move: v3 is now empty
// After move, v3 is in valid but unspecified state
// You can still use v3, but don't assume it has any particular value
v3.clear(); // OK
v3.push_back(10); // OK
// Common pattern: moving in function returns
std::vector<int> createVector() {
std::vector<int> local = {1, 2, 3, 4, 5};
return local; // No std::move needed! Compiler does RVO/NRVO
}
// When to use std::move
class Widget {
std::vector<int> data;
public:
void setData(std::vector<int> newData) {
data = std::move(newData); // Move parameter into member
}
};
Universal references (also called forwarding references) can bind to both lvalues and rvalues, preserving their value category. It's like a postal service that handles both regular mail and express packages appropriately.
// Universal reference: T&& where T is deduced
template<typename T>
void process(T&& param) { // Universal reference
// param can be lvalue or rvalue reference
}
// Perfect forwarding: preserve value category
template<typename T>
void wrapper(T&& arg) {
// std::forward preserves whether arg was lvalue or rvalue
actualFunction(std::forward<T>(arg));
}
// Practical example: make_unique before C++14
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// Usage
auto p1 = make_unique<String>("Hello"); // Forward rvalue
String s = "World";
auto p2 = make_unique<String>(s); // Forward lvalue (copies)
auto p3 = make_unique<String>(std::move(s)); // Forward rvalue (moves)
All standard containers support move semantics, making operations like returning containers from functions extremely efficient.
// Efficient container operations
std::vector<std::string> readFile(const std::string& filename) {
std::vector<std::string> lines;
// ... read file into lines ...
return lines; // Move, not copy! (NRVO or move)
}
// Container of containers
std::vector<std::vector<int>> matrix;
std::vector<int> row = {1, 2, 3, 4, 5};
// Old way (copy)
matrix.push_back(row); // row is still valid
// New way (move)
matrix.push_back(std::move(row)); // row is now empty
// Moving elements within containers
std::vector<std::string> v1 = {"apple", "banana", "cherry"};
std::vector<std::string> v2;
// Move all elements
v2 = std::move(v1); // v1 is now empty
// Move individual elements
std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>()); // Move-only type
// Algorithms with move
std::vector<std::string> source = {"a", "b", "c"};
std::vector<std::string> dest;
std::move(source.begin(), source.end(), std::back_inserter(dest));
class String {
// ... other members ...
// Unified assignment operator handles both copy and move
String& operator=(String other) { // Pass by value
swap(other);
return *this;
} // other's destructor cleans up our old data
void swap(String& other) noexcept {
std::swap(data, other.data);
std::swap(length, other.length);
}
};
class FileHandle {
FILE* file;
public:
// Delete copy operations
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Default move operations
FileHandle(FileHandle&&) = default;
FileHandle& operator=(FileHandle&&) = default;
// Now FileHandle can only be moved, not copied
// Perfect for unique resources like file handles
};
template<typename T>
class Buffer {
private:
T* data;
size_t size;
public:
// Constructor
explicit Buffer(size_t n)
: data(new T[n]), size(n) {}
// Destructor
~Buffer() { delete[] data; }
// TODO: Implement
// 1. Copy constructor
// 2. Copy assignment
// 3. Move constructor
// 4. Move assignment
// Hints:
// - Copy should allocate new memory and copy elements
// - Move should transfer ownership and set source to nullptr
// - Remember to handle self-assignment
// - Mark move operations as noexcept
};
// Create a factory function that perfectly forwards arguments
template<typename T, typename... Args>
T* factory(Args&&... args) {
std::cout << "Creating object with "
<< sizeof...(args) << " arguments\n";
// TODO: Use perfect forwarding to construct T
// Hint: Use std::forward<Args>(args)...
}
// Test with:
struct Widget {
Widget(int x) { std::cout << "Widget(int)\n"; }
Widget(const std::string& s) { std::cout << "Widget(string&)\n"; }
Widget(std::string&& s) { std::cout << "Widget(string&&)\n"; }
};
// Implement a thread-safe queue for move-only types
template<typename T>
class MoveOnlyQueue {
private:
std::queue<T> queue;
mutable std::mutex mutex;
public:
// Only allow moving in
void push(T&& value) {
// TODO: Lock mutex and move value into queue
}
// Only allow moving out
std::optional<T> pop() {
// TODO: Lock mutex, check if empty
// If not empty, move front element out
// Return std::nullopt if empty
}
};
Move semantics can dramatically improve performance, especially for containers and resource-heavy objects.
// Benchmark example
#include <chrono>
#include <vector>
void benchmarkCopyVsMove() {
const size_t SIZE = 1000000;
std::vector<int> source(SIZE, 42);
// Benchmark copy
auto start = std::chrono::high_resolution_clock::now();
std::vector<int> copied = source; // Copy
auto copy_time = std::chrono::high_resolution_clock::now() - start;
// Benchmark move
start = std::chrono::high_resolution_clock::now();
std::vector<int> moved = std::move(source); // Move
auto move_time = std::chrono::high_resolution_clock::now() - start;
std::cout << "Copy time: "
<< std::chrono::duration_cast<std::chrono::microseconds>(copy_time).count()
<< " μs\n";
std::cout << "Move time: "
<< std::chrono::duration_cast<std::chrono::microseconds>(move_time).count()
<< " μs\n";
// Typical result:
// Copy time: 2000 μs
// Move time: 1 μs
}
const std::vector<int> v = {1, 2, 3};
std::vector<int> v2 = std::move(v); // Doesn't move! Copies instead!
// std::move on const object returns const T&&, which matches copy constructor
std::string s = "Hello";
std::string s2 = std::move(s);
std::cout << s; // Undefined behavior? No! But s is empty
// Standard library types guarantee valid but unspecified state
// BAD - prevents RVO
std::vector<int> bad() {
std::vector<int> v = {1, 2, 3};
return std::move(v); // Don't do this!
}
// GOOD - allows RVO/NRVO
std::vector<int> good() {
std::vector<int> v = {1, 2, 3};
return v; // Compiler optimizes
}
You've mastered the essentials of move semantics and rvalue references:
Move semantics transformed C++ from a language that copied everything to one that can efficiently transfer resources. It's like upgrading from photocopying documents to simply handing them over. In our next lesson, we'll explore Design Patterns in Modern C++, showing how to structure your code for maximum flexibility and maintainability.