Lesson 48
Move Semantics
Covers lvalues and rvalues, rvalue references, std::move(), move constructors, move assignment operators, and when to prefer moving over copying.
Lesson content
What are Move Semantics in C++?
Move semantics allow a program to move resources (such as dynamically allocated memory) from one object to another instead of creating a complete copy.
After a move operation, the destination object receives the resource, while the source object remains in a valid but unspecified state and can be safely destroyed or assigned a new value.
Think of moving house. Instead of buying all new furniture (copying), you simply transport your existing furniture to the new house (moving). Moving is usually much faster than replacing everything.
Why do Move Semantics exist?
Before C++11, objects could only be copied. Consider a large vector containing one million integers. Copying it creates another million-element vector, which takes both time and memory. Move semantics solve this by transferring ownership of the internal memory instead of duplicating it.
- Faster execution
- Reduced memory usage
- Fewer unnecessary copies
- Better performance for STL containers
Real-world use cases
- STL containers (vector, string, map)
- Smart pointers
- Resource management
- File handling
- Network applications
- Game engines
- Database systems
- High-performance software
Prerequisites
- Variables and data types
- Classes and objects
- Constructors
- Copy constructors
- Dynamic memory (new and delete)
- Pointers and references
- Smart pointers (recommended)
Lvalues and rvalues
Understanding move semantics starts with lvalues and rvalues.
An lvalue refers to an object that has a name and occupies memory. In int x = 10;, x is an lvalue because it has a memory location.
An rvalue is a temporary value that does not have a persistent memory location, such as 10 or x + 5. Temporary values can be moved because they are about to be destroyed.
Rvalue references
C++11 introduced rvalue references, represented by &&. An rvalue reference can bind to temporary objects and enables move semantics.
int&& value = 100;std::move()
std::move() does not move an object by itself. Instead, it casts an object to an rvalue reference, making it eligible for move operations.
std::move(object);Move constructor
A move constructor transfers resources from one object to another. Instead of copying data, it transfers ownership.
ClassName(ClassName&& other);Move assignment operator
The move assignment operator transfers resources to an existing object. It is used when assigning one object to another after both have already been created.
ClassName& operator=(ClassName&& other);Copy vs move
- Copy: duplicates data, is slower, results in two separate resources, and leaves the source unchanged
- Move: transfers ownership, is faster, results in one transferred resource, and leaves the source valid but unspecified
Example 1: Using std::move()
#include <iostream>
#include <string>
#include <utility>
using namespace std;
int main()
{
string first = "Hello";
string second = move(first);
cout << "Second: " << second << endl;
cout << "First: " << first << endl;
return 0;
}
// Sample Output:
// Second: Hello
// First:
// Note: The exact contents of "first" after the move are valid but
// unspecified. Many implementations leave it empty, but you should
// not rely on that behavior.Example 2: Moving a vector
#include <iostream>
#include <vector>
#include <utility>
using namespace std;
int main()
{
vector<int> first = {1,2,3,4};
vector<int> second = move(first);
cout << second.size() << endl;
cout << first.size() << endl;
return 0;
}
// Possible Output:
// 4
// 0
// Note: Many standard library implementations leave the moved-from
// vector empty, but only its validity—not its exact state—is guaranteed.Example 3: Custom move constructor
#include <iostream>
using namespace std;
class Number
{
private:
int* data;
public:
Number(int value)
{
data = new int(value);
}
Number(Number&& other) noexcept
{
data = other.data;
other.data = nullptr;
}
~Number()
{
delete data;
}
void display()
{
if (data)
cout << *data << endl;
}
};
int main()
{
Number first(100);
Number second(move(first));
second.display();
return 0;
}Example 4: Move assignment operator
#include <iostream>
using namespace std;
class Resource
{
private:
int* data;
public:
Resource(int value)
{
data = new int(value);
}
Resource(Resource&& other) noexcept
{
data = other.data;
other.data = nullptr;
}
Resource& operator=(Resource&& other) noexcept
{
if (this != &other)
{
delete data;
data = other.data;
other.data = nullptr;
}
return *this;
}
~Resource()
{
delete data;
}
};
int main()
{
Resource first(10);
Resource second(20);
second = move(first);
return 0;
}Example 5: Returning large objects
#include <iostream>
#include <vector>
using namespace std;
vector<int> createData()
{
vector<int> values(1000000, 1);
return values;
}
int main()
{
vector<int> data = createData();
cout << data.size() << endl;
return 0;
}
// Note: Modern C++ compilers often apply copy elision (especially
// guaranteed in many cases since C++17), so no copy or move may
// actually occur here.Common mistakes and pitfalls
- Using std::move() on an object and then assuming it still contains its original value. Fix: only use a moved-from object in ways guaranteed to be valid (e.g., assign a new value or destroy it)
- Forgetting to set the source pointer to nullptr in a move constructor. Fix: set the source pointer to nullptr after transferring ownership
- Copying large objects unnecessarily. Fix: move them when ownership is no longer needed
- Writing move operations without considering self-assignment. Fix: check this != &other in move assignment operators
- Omitting noexcept on move operations when appropriate. Fix: mark move constructors and move assignment operators noexcept whenever possible to improve STL optimizations
Best practices
- Prefer move semantics for large objects that no longer need their original ownership
- Use std::move() only when you intentionally want to transfer ownership
- Prefer standard library types (std::string, std::vector, smart pointers), which already implement efficient move operations
- Mark move constructors and move assignment operators as noexcept when possible
- Leave moved-from objects in a valid state, such as by setting raw pointers to nullptr
When not to use this
- You still need the original object's value
- The object is small and inexpensive to copy (for example, an int)
- The object does not own any resources
- Copying is required to preserve two independent objects
Summary
- Move Semantics in C++ transfer ownership of resources instead of copying them
- They improve performance by avoiding unnecessary copies of large objects
- std::move() converts an object into an rvalue reference, making it eligible for moving
- Move constructors initialize new objects by taking ownership of resources
- Move assignment operators transfer ownership between existing objects
- Moved-from objects remain valid but in an unspecified state
- Modern C++ libraries use move semantics extensively to improve efficiency
Frequently asked questions
What is the difference between copying and moving?
Copying creates a completely new copy of the data, while moving transfers ownership of existing resources without duplicating them.
Does std::move() actually move an object?
No. std::move() simply casts an object to an rvalue reference. The actual move happens only if a move constructor or move assignment operator is available.
Can I use an object after calling std::move()?
Yes. A moved-from object remains valid, but its contents are unspecified. You can destroy it, assign a new value to it, or call operations documented as valid for moved-from objects. You should not assume it still contains its previous data.
Why are move constructors often marked noexcept?
Many STL containers, such as std::vector, prefer move operations only when they are guaranteed not to throw exceptions. Marking move operations noexcept can improve performance during container reallocations.
Do I always need to write my own move constructor?
No. Many classes don't need a custom move constructor. If your class only contains members that already support moving (such as std::string, std::vector, or smart pointers), the compiler-generated move operations are often sufficient. You typically write custom move operations only when your class directly manages resources, such as raw pointers or file handles.