Move Semantics
Master C++11 move semantics to efficiently transfer resources instead of copying them
Move semantics revolutionize C++ performance by enabling efficient transfer of resources from temporary objects instead of expensive copying operations.
A Simple Example
#include <iostream>
#include <cstring>
class String {
private:
char* data;
int length;
public:
String(const char* str = "") {
length = std::strlen(str);
data = new char[length + 1];
std::strcpy(data, str);
std::cout << "Constructor: " << data << "\n";
}
String(const String& other) {
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
std::cout << "Copy constructor: " << data << "\n";
}
String(String&& other) noexcept {
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
std::cout << "Move constructor" << "\n";
}
String& operator=(String&& other) noexcept {
if (this == &other) {
return *this;
}
delete[] data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
std::cout << "Move assignment" << "\n";
return *this;
}
~String() {
std::cout << "Destructor" << "\n";
delete[] data;
}
void display() const {
if (data) {
std::cout << data << " (length: " << length << ")" << "\n";
} else {
std::cout << "(moved-from object)" << "\n";
}
}
};
String createString() {
return String{"Temporary"};
}
int main() {
String s1{"Hello"};
String s2 = std::move(s1);
std::cout << "s1: "; s1.display();
std::cout << "s2: "; s2.display();
String s3 = createString();
s3.display();
return 0;
}
Breaking It Down
Rvalue References &&
- What it is: A reference to a temporary object that's about to be destroyed
-
Syntax:
Type&&declares an rvalue reference parameter - Key insight: Rvalue references let you detect when an object can be stolen from
- Remember: Lvalues have names and persist, rvalues are temporary and nameless
Move Constructor T(T&& other)
- What it does: Transfers resources from a temporary to a new object
- How it works: Steal pointers from source, set source pointers to nullptr
- Performance: O(1) pointer copy vs O(n) deep copy for large data
- Remember: Always mark noexcept - moves should never throw exceptions
Move Assignment T& operator=(T&& other)
- What it does: Transfers resources to an existing object
- Critical step: Clean up existing resources before stealing new ones
-
Self-assignment check: Always check
if (this == &other)first - Remember: Leave the source in a valid but unspecified state
std::move() - The Resource Transfer
- What it does: Casts an lvalue to an rvalue reference, enabling moves
- Important: std::move() doesn't actually move anything - it enables moving
- After std::move(): The source object is still valid but in an unspecified state
- Remember: Don't use an object after moving from it unless you reinitialize it
Why This Matters
- Copying large objects is expensive - allocate new memory, copy all data, deallocate old memory.
- But what if you're copying from a temporary that's about to be destroyed anyway?
- Move semantics let you steal resources from temporaries instead of copying.
- This is a game-changer for performance, especially with containers like vectors and strings.
- Understanding move semantics is essential for writing modern, efficient C++ and is a key part of the Rule of Five.
Critical Insight
Move semantics don't move memory - they move pointers! Instead of copying megabytes of data byte-by-byte, you just copy a single 8-byte pointer and null out the source.
Think of it like transferring house ownership: instead of building an identical house and moving all furniture (copy), you just hand over the keys and the original owner moves out (move). The house stays exactly where it is, only the ownership changes.
This is why moved-from objects are valid but unspecified - they're empty shells with no resources.
Best Practices
Always mark move operations noexcept: This enables optimizations in standard containers. Move constructors and assignments should never throw exceptions.
Null out source pointers: After stealing resources, set the source pointers to nullptr to prevent double-deletion crashes.
Check for self-assignment: Always start move assignment with if (this == &other) return *this; to handle edge cases.
Leave moved-from objects valid: The source should be in a state where its destructor can safely run, even if its values are unspecified.
Use std::move() explicitly for lvalues: Don't rely on implicit moves - be explicit when you want to move from a named object.
Common Mistakes
Forgetting to null out pointers: The most common bug. After data = other.data;, you must do other.data = nullptr; to prevent double-deletion.
Using moved-from objects: After std::move(), the source is in an unspecified state. Don't access its values unless you reinitialize it first.
Missing noexcept: Without noexcept, containers won't use your move operations and will fall back to expensive copying.
Not cleaning up in move assignment: Move assignment must delete existing resources before stealing new ones to prevent memory leaks.
Debug Challenge
This move constructor has a critical bug that will cause a crash. Click the highlighted line to fix it:
Quick Quiz
- What is the primary purpose of move semantics?
- What does std::move() actually do?
- After moving from an object with std::move(), what state is it in?
- Why should move constructors be marked noexcept?
Practice Playground
Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.
Output:
Error:
Lesson Progress
- Fix This Code
- Quick Quiz
- Practice Playground - run once