Hello! Welcome to our next lesson in the "Modern C++" module.
In our previous lessons, we explored std::unique_ptr and std::shared_ptr. You saw how std::move was essential for transferring ownership of a unique_ptr. This was our first glimpse into a powerful C++11 feature that fundamentally changed how we write efficient and expressive code.
Today, we'll dive deep into that mechanism. The learning outcome for this lesson is to explain move semantics (rvalue references) and the purpose of std::move. This topic is a perfect example of understanding what happens "under the hood" to write better, high-performance code, which I know is one of your key interests. We'll demystify what std::move actually does and explore the concepts that make it possible.
1. The Problem: Unnecessary and Expensive Copying
Before C++11, many common operations involved creating temporary objects that were immediately copied and then destroyed. Imagine a function that builds and returns a large data structure, like a vector containing thousands of strings.
std::vector<std::string> create_data() {
std::vector<std::string> data;
// ... loop that adds 10,000 strings to 'data' ...
return data; // In C++98, this could trigger a deep copy
}
int main() {
std::vector<std::string> my_data = create_data();
}
In older C++, the return data; statement could trigger a "deep copy":
- A new
std::vectoris created formy_data. - The program allocates new memory for the
my_datavector's internal array. - It then iterates through all 10,000 strings in the original
datavector, allocating memory for each string inmy_dataand copying the character data over. - Finally, the temporary
dataobject is destroyed, deallocating all of its original memory.
This is incredibly inefficient. We build a complex object, make a complete copy of it, and then immediately throw the original away. Move semantics were introduced to solve exactly this problem.
To see a clear visual explanation of this performance issue, watch the first part of the following video.
C++ Move Semantics Explained: std::move, Lvalues & Rvalues
The video 'C++ Move Semantics Explained' by DeepDiveDev starts by perfectly illustrating the problem of deep copying temporary objects.
Watch from the beginning until 01:54. Focus on how a 'deep structure' like a vector of strings makes copying so expensive.
The core idea of move semantics is this: if the source of a copy is a temporary object that's about to be destroyed anyway, why copy its data? Why not just "steal" or "move" its resources?
2. The Foundation: Lvalues and Rvalues
To enable this "stealing," the compiler needs a way to distinguish between objects that have a persistent identity and temporary objects that are about to disappear. This is where value categories come in. Every expression in C++ is either an lvalue or an rvalue.
-
Lvalue (Locator value): An expression that refers to an object with a persistent identity and memory location. You can take its address. Think of it as anything you can put on the left side of an assignment.
- Examples: Variables like
int x;orstd::string name;. Bothxandnameare lvalues.
- Examples: Variables like
-
Rvalue (Right value): An expression that refers to a temporary object, a literal, or the result of an expression that is about to be discarded. You cannot take its address.
- Examples: Literals like
42, temporary objects likestd::string("hello"), or the result of an expression likex + y.
- Examples: Literals like
C++11 introduced a new type of reference, the rvalue reference (written as &&), which can only bind to rvalues. This allows us to overload functions to behave differently for lvalues and rvalues.
void func(const MyType& obj);// Overload for lvalues (traditional copy)void func(MyType&& obj);// Overload for rvalues (new move)
This distinction is the key to how move semantics work automatically for temporary objects.
C++ Move Semantics Explained: std::move, Lvalues & Rvalues
Let's return to the DeepDiveDev video for a great explanation of lvalues, rvalues, and how rvalue references (&&) allow the compiler to choose between copying and moving.
Watch from 02:47 to 05:06. Pay close attention to the core distinction: lvalues have an identity (you can take their address), while rvalues do not.
3. Implementing a "Move"
So, what does it mean to "move" an object? For objects that manage heap-allocated resources (like our vector or your smart pointers), a move is a cheap transfer of ownership. Instead of a deep copy, it's a shallow copy of pointers, followed by nulling out the source object.
Let's look at how you would implement this with a move constructor and move assignment operator.
-
Move Constructor:
MyClass(MyClass&& other) noexcept- Takes an rvalue reference (
&&) to another object. - "Steals" the resources from
other(e.g., copies its internal pointers). - Puts
otherinto a valid but empty state (e.g., sets its pointers tonullptr). This prevents the destructor ofotherfrom freeing the resources we just stole.
- Takes an rvalue reference (
-
Move Assignment Operator:
MyClass& operator=(MyClass&& other) noexcept- Similar logic, but for assignment (
a = std::move(b);). It must also handle releasing its own existing resources before taking ownership of the new ones.
- Similar logic, but for assignment (
The noexcept keyword is critical. It's a promise to the compiler that your move operation won't throw an exception. Standard library containers like std::vector rely on this promise to safely use your move operations during reallocations. Without noexcept, they will fall back to copying, defeating the purpose.
std::move doesn't move anything: A deep dive into Value Categories
The article 'std::move doesn't move anything' provides an excellent example of implementing the 'Rule of Five' (destructor, copy/move constructors, copy/move assignment operators). This will show you exactly how a move is implemented in code.
Read the section titled 'Implementing Move Semantics Correctly'. Focus on the implementation of the Resource class, especially the move constructor and move assignment operator. Pay attention to the use of std::exchange and the explanation for why noexcept is so important.
4. std::move: Permission to Steal
We now know the compiler automatically uses move semantics for temporary rvalues. But what if we have an lvalue (a named object) that we want to move from? For example, you have a large object and you want to pass its contents to another object and you're finished with the original.
This is where std::move comes in.
Despite its name, std::move does not move anything. It's just a cast. Specifically, std::move unconditionally casts its argument to an rvalue reference.
std::string a = "hello";
std::string b = std::move(a); // This is what happens:
// 1. std::move(a) casts the lvalue 'a' into an rvalue reference.
// 2. This allows the compiler to select the move constructor of std::string.
// 3. The move constructor of std::string is called, which steals the internal
// character buffer from 'a' and leaves 'a' in an empty state.
By using std::move(a), you are making a promise to the compiler: "I am done with this object a. You can treat it as a temporary and pillage its resources."
This changes the value category of the expression from an lvalue to a special kind of rvalue called an xvalue (eXpiring value). An xvalue is an object that has an identity but is marked for resource stealing.

The State of a Moved-From Object
After an object has been moved from, it is in a valid but unspecified state.
- Valid: The object is not broken. Its invariants hold. You can safely call its destructor or assign a new value to it.
- Unspecified: You cannot make any assumptions about its contents. For a
std::string, it's probably empty, but the standard doesn't guarantee it.
The rule is simple: after you std::move from an object, do not use it again unless you assign a new value to it or let it be destroyed.
For an excellent, in-depth discussion on this, let's turn to a CppCon talk.
Back to Basics: Move Semantics - Nicolai Josuttis - CppCon 2021
Nicolai Josuttis gives a masterclass on move semantics. He clearly explains why we need std::move for named objects and what 'valid but unspecified state' means in practice.
Watch from 09:47 to 20:00. The first part covers the need for std::move. The second part (starting at 14:16) provides a fantastic explanation of the moved-from state and shows a practical example of reusing a moved-from string in a loop with getline.
5. Common Pitfalls
Move semantics are a huge performance win, but misusing std::move can be counter-productive. Here are two of the most common mistakes.
-
return std::move(local_variable);
You might think you are helping the compiler, but you are actually preventing a crucial optimization called (Named) Return Value Optimization (NRVO). When youreturn local_variable;, the compiler can often construct the object directly in the caller's memory, avoiding both a copy and a move. It's a zero-cost operation. By addingstd::move, you force a move, turning a zero-cost operation into a low-cost one. Neverstd::movea local variable in a return statement. -
Moving from a
constobject
Moving is a mutating operation—it modifies the source object. Aconstobject, by definition, cannot be modified. If you trystd::moveon aconstobject, the compiler cannot use the move constructor (which takes a non-constT&&). Instead, it silently falls back to the copy constructor (which takes aconst T&). Your code will compile but will perform a slow copy instead of a fast move.
The article we read earlier has a great section summarizing these mistakes. I recommend reviewing it now as a consolidation of these key rules.
std::move doesn't move anything: A deep dive into Value Categories
Let's revisit the 'std::move doesn't move anything' article to review the most common mistakes.
Read the section titled 'The Naive Approach: Three Mistakes That Will Hurt Your Performance'. This is a concise summary of what not to do.
Conclusion
You've just learned one of the most important C++11 features for writing high-performance code. Understanding move semantics is crucial for working effectively with the C++ standard library and for writing your own efficient resource-managing classes.
Key Takeaways:
- Move semantics avoid expensive deep copies by transferring ownership of resources from temporary or explicitly "moved" objects.
- The mechanism relies on rvalue references (
&&), which allow function overloading to distinguish between lvalues (persistent objects) and rvalues (temporaries). - A move operation (in a move constructor or assignment operator) typically involves a cheap "shallow copy" of pointers and setting the source object to an empty state. These should always be marked
noexcept. std::moveis not a move; it's a cast. It gives you permission to treat an lvalue as an rvalue, enabling a move.- An object that has been moved from is in a valid but unspecified state. Don't use its value again without re-assigning it.
Next Up
In this lesson, we briefly saw code snippets mentioning std::forward and templates with T&&. It turns out that when T&& is used in a template context, it becomes a special "forwarding reference" that can bind to both lvalues and rvalues. In our next lesson, we will explore function templates, and you'll see how std::forward is used with these references to write generic code that perfectly preserves the original value category of arguments.