Lvalues vs Rvalues
Before diving into rvalue references, you need to understand the fundamental distinction between lvalues and rvalues. This concept is the foundation of move semantics.
What are Lvalues and Rvalues?
Every expression in C++ has a value category. The two most important categories are:
Has identity - refers to a memory location
- Has a name you can refer to
- You can take its address with &
- Persists beyond the expression
- Can appear on the left side of assignment
No identity - temporary, about to expire
- No name (usually)
- Cannot take its address
- Destroyed at end of expression
- Can only appear on the right side
#include <iostream>
#include <string>
int main() {
// LVALUES - have identity, persist
int x = 10; // x is an lvalue
int* ptr = &x; // Can take address of lvalue
int& ref = x; // Can bind lvalue reference to lvalue
int arr[5] = {1, 2, 3, 4, 5};
arr[0] = 100; // arr[0] is an lvalue
std::string name = "Alice";
name[0] = 'E'; // name[0] is an lvalue
This code demonstrates lvalues - expressions with identity that persist beyond their use. Variables
like x and name are lvalues because they have memory addresses you can
take with &. Array elements like arr[0] and string characters like
name[0] are also lvalues because they refer to specific memory locations. You can
modify them, take their address, and create references to them.
// RVALUES - temporary, no identity
// 42; // Literal 42 is an rvalue
// x + 5; // Expression result is an rvalue
// getName(); // Return value (if not reference) is an rvalue
int y = 42; // 42 is rvalue, assigned to lvalue y
int z = x + 5; // (x + 5) is rvalue, assigned to lvalue z
Rvalues are temporary values without identity. Literals like 42 and expression results
like x + 5 are rvalues - they exist only momentarily during computation and then disappear.
You can use them on the right side of assignments (hence "rvalue"), but they don't have memory addresses
you can capture. Function return values (when not returning by reference) are also rvalues because
they're temporary objects about to be destroyed.
// These would NOT compile:
// 42 = x; // Error: cannot assign to rvalue
// int* p = &42; // Error: cannot take address of rvalue
// int& r = 42; // Error: cannot bind lvalue ref to rvalue
return 0;
}
These lines (commented out) would cause compilation errors because they violate fundamental C++ rules.
You cannot assign to an rvalue like 42 because it's not a memory location - where would
the value go? You can't take the address of 42 because it doesn't exist in memory - it's
just a constant value. And you can't bind a regular lvalue reference to 42 because references
must point to objects that persist, not temporaries that immediately disappear.
Simple Mental Model
| Expression | Category | Why? |
|---|---|---|
x (variable) |
lvalue | Has a name, can take &x |
arr[i] |
lvalue | Refers to specific memory, can take &arr[i] |
*ptr |
lvalue | Dereference gives identity, can take &(*ptr) |
++x (pre-increment) |
lvalue | Returns reference to x itself |
42 (literal) |
rvalue | No memory location, cannot take &42 |
x + y |
rvalue | Temporary result, cannot take &(x+y) |
x++ (post-increment) |
rvalue | Returns copy of old value, not x itself |
std::string("hello") |
rvalue | Temporary object, about to be destroyed |
Why Does This Matter?
The distinction between lvalues and rvalues becomes crucial when we want to optimize our code. Consider this scenario:
#include <iostream>
#include <vector>
#include <string>
std::vector<int> createLargeVector() {
std::vector<int> v(1000000); // 1 million integers
// ... fill vector with data ...
return v; // Returns a temporary (rvalue)
}
int main() {
// Old C++ (pre-C++11) would COPY the entire vector
// That's 4MB of unnecessary copying!
std::vector<int> result = createLargeVector();
// With move semantics, we can DETECT that createLargeVector()
// returns a temporary (rvalue) and STEAL its resources
// instead of copying. The temporary is about to die anyway!
return 0;
}
Without Move Semantics
Temporary vector created, then copied (4MB), then destroyed. Wasteful!
With Move Semantics
Temporary's internal pointer is moved (just a pointer copy). Near instant!
Practice Questions: Lvalues vs Rvalues
Given:
int arr[] = {10, 20, 30, 40, 50};
Task: Write a function getElement(int* arr, int index) that returns a reference to the element at the given index, allowing modification through the return value.
Expected output when calling getElement(arr, 2) = 100;: Array becomes {10, 20, 100, 40, 50}
Show Solution
#include <iostream>
int& getElement(int* arr, int index) {
return arr[index]; // Returns lvalue reference
}
int main() {
int arr[] = {10, 20, 30, 40, 50};
getElement(arr, 2) = 100; // Modify through reference
for (int x : arr) std::cout << x << " ";
// Output: 10 20 100 40 50
}
Given:
std::string createMessage(const std::string& name) {
return "Hello, " + name + "!";
}
Task: Call createMessage and store the result in a const lvalue reference. Then print the message length and content to prove the temporary's lifetime was extended.
Expected output for name "Alice": Length: 13, Message: Hello, Alice!
Show Solution
#include <iostream>
#include <string>
std::string createMessage(const std::string& name) {
return "Hello, " + name + "!";
}
int main() {
// Const reference extends temporary lifetime
const std::string& msg = createMessage("Alice");
// Temporary is still valid here!
std::cout << "Length: " << msg.length()
<< ", Message: " << msg << std::endl;
// Output: Length: 13, Message: Hello, Alice!
}
Given:
int x = 10;
const int cx = 20;
Task: Write three overloaded functions named identify that detect and print whether the argument is: (1) non-const lvalue, (2) const lvalue, or (3) rvalue. Test with x, cx, and x + cx.
Expected output: non-const lvalue, const lvalue, rvalue
Show Solution
#include <iostream>
void identify(int&) {
std::cout << "non-const lvalue" << std::endl;
}
void identify(const int&) {
std::cout << "const lvalue" << std::endl;
}
void identify(int&&) {
std::cout << "rvalue" << std::endl;
}
int main() {
int x = 10;
const int cx = 20;
identify(x); // non-const lvalue
identify(cx); // const lvalue
identify(x + cx); // rvalue
}
Rvalue References (&&)
Rvalue references, declared with &&, are a new type of reference introduced in C++11 that can bind to temporary objects. They are the key to enabling move semantics.
The && Syntax
C++11 introduced a brand new reference type using the && syntax (two ampersands).
If you're familiar with regular references (&, one ampersand), which are technically
called "lvalue references," then rvalue references (&&) are their counterpart.
Here's the key difference: while lvalue references can only bind to lvalues (named objects with identity), rvalue references can only bind to rvalues (temporary objects about to be destroyed). This new ability to "grab hold" of temporaries is what enables move semantics!
Think of it this way: regular references (&) let you create an alias to an existing
object. Rvalue references (&&) let you grab onto temporary objects that are about
to disappear, giving you a chance to salvage their resources before they're destroyed.
| Reference Type | Syntax | Binds To | Primary Use |
|---|---|---|---|
| Lvalue Reference | T& |
Lvalues only | Aliasing existing objects |
| Const Lvalue Reference | const T& |
Both lvalues and rvalues | Read-only access, accepting any value |
| Rvalue Reference | T&& |
Rvalues only | Move semantics, detecting temporaries |
#include <iostream>
#include <string>
int main() {
int x = 10;
// Lvalue reference - binds to lvalue
int& lref = x; // OK: x is an lvalue
// int& lref2 = 42; // ERROR: 42 is an rvalue
Regular lvalue references (&) can only bind to lvalues - named objects with identity.
Here, lref successfully binds to x because x is a variable
with a memory address. However, trying to bind to 42 fails because literals are rvalues
with no identity. This is the traditional C++ reference behavior you're familiar with.
// Const lvalue reference - binds to both
const int& clref1 = x; // OK: lvalue
const int& clref2 = 42; // OK: rvalue (special rule)
Const lvalue references (const T&) have a special power - they can bind to both lvalues
AND rvalues! This is an exception in C++ that makes const references very flexible. When you bind
a const reference to an rvalue like 42, the compiler creates a hidden temporary variable
to hold that value, and the reference points to it. This is why const references are commonly used
for function parameters.
// Rvalue reference - binds to rvalue
int&& rref = 42; // OK: 42 is an rvalue
// int&& rref2 = x; // ERROR: x is an lvalue
Rvalue references (&&) are the opposite of lvalue references - they can ONLY bind to
rvalues (temporaries). Here, rref successfully binds to the literal 42,
but attempting to bind to x fails because x is an lvalue. This selectivity
is intentional - it lets the compiler distinguish between "I can safely steal from this" (rvalue)
versus "I must be careful with this" (lvalue).
// We can modify through rvalue reference!
rref = 100;
std::cout << rref << std::endl; // Output: 100
// String example
std::string&& strRef = std::string("temporary");
std::cout << strRef << std::endl; // Output: temporary
return 0;
}
Unlike const references, rvalue references allow modification! Once we've bound rref to
the temporary, we can modify it just like a regular variable. This demonstrates that rvalue references
give you full access to temporaries before they're destroyed. The string example shows the same concept -
we bind to a temporary string object and can use it normally. These temporaries live until the reference
goes out of scope.
rref above is an lvalue (it has a name and address), even though it's
declared as int&&.
Extending Temporary Lifetime
One of the most useful features of both const T& and T&& is their
ability to extend the lifetime of temporary objects. This is a special rule
in C++ that might seem like magic at first!
Normally, a temporary object (like the return value of a function) is destroyed at the end of the statement where it was created. But when you bind that temporary to a reference, C++ says: "Okay, I'll keep this object alive as long as the reference exists." This prevents dangling references and makes your code safer.
Why is this useful? It lets you work with temporary objects as if they were regular variables,
extending their usefulness beyond a single statement. You can inspect them, modify them (with
T&&), and even pass them to other functions - all while the temporary is guaranteed
to stay alive.
#include <iostream>
#include <string>
std::string createGreeting() {
return "Hello, World!"; // Returns temporary
}
This simple function returns a string by value, which means it returns a temporary object. Normally, this temporary would be destroyed immediately after the function call completes. But we can use rvalue references to capture and extend the lifetime of this temporary, allowing us to work with it beyond a single statement.
int main() {
// Without binding - temporary destroyed immediately
// createGreeting(); // Temporary exists only for this line
// Bind to rvalue reference - lifetime extended!
std::string&& greeting = createGreeting();
By binding the returned temporary to an rvalue reference, we trigger C++'s lifetime extension rule.
The temporary string that createGreeting() returns would normally be destroyed at the
semicolon, but because we bind it to greeting, C++ keeps it alive for the entire scope.
This is safe and efficient - no copying happens, just a reference to the temporary.
// greeting is valid for the rest of this scope
std::cout << greeting << std::endl; // Safe to use
// Can even modify it
greeting += " How are you?";
std::cout << greeting << std::endl; // "Hello, World! How are you?"
return 0;
} // greeting destroyed here
The extended temporary behaves just like a regular variable - we can read it, modify it, and pass it
around. The modification greeting += " How are you?"; works because rvalue references
aren't const. This temporary string stays alive until the reference goes out of scope at the closing
brace. This pattern is incredibly useful for capturing function return values efficiently.
Function Overloading with References
One of the most powerful techniques with rvalue references is function overloading. You can write multiple versions of the same function - one that handles lvalues and another that handles rvalues. The compiler will automatically choose the right version based on what you pass in!
Why would you do this? Because you can optimize each version differently. The lvalue version might need to copy data (since the original is still being used), while the rvalue version can steal resources (since the temporary is about to be destroyed anyway). This is the foundation of move semantics!
Think of it like having two doors: a "copy entrance" for valuable items you still need, and an "express entrance" for things you're throwing away. The compiler automatically routes your objects to the right door.
#include <iostream>
#include <string>
// Overload for lvalues - receives existing objects
void process(const std::string& str) {
std::cout << "Lvalue version: " << str << std::endl;
}
// Overload for rvalues - receives temporaries
void process(std::string&& str) {
std::cout << "Rvalue version: " << str << std::endl;
// We can "steal" from str here since it's temporary!
}
Here we define two versions of the process function - one accepting const std::string&
(for lvalues) and one accepting std::string&& (for rvalues). The compiler uses overload
resolution to automatically select the right version based on the argument's value category. The lvalue
version takes a const reference because we shouldn't modify objects that the caller might still need.
The rvalue version can be optimized differently since it receives temporaries.
int main() {
std::string name = "Alice";
process(name); // Calls lvalue version
process("Bob"); // Calls rvalue version (literal -> temporary)
process(std::string("Eve")); // Calls rvalue version (explicit temporary)
process(name + "!"); // Calls rvalue version (concatenation = temporary)
return 0;
}
// Output:
// Lvalue version: Alice
// Rvalue version: Bob
// Rvalue version: Eve
// Rvalue version: Alice!
The compiler's overload resolution automatically routes each call to the appropriate version. When we
pass name, an lvalue, the const reference version is called. When we pass string literals,
explicit temporaries like std::string("Eve"), or expression results like name + "!",
the rvalue reference version is called. This automatic dispatch is the key to efficient move semantics -
the compiler knows when it's safe to optimize (rvalues) versus when it must be careful (lvalues).
LVALUE
has name / identityRVALUE
temporary / expiringT&
Lvalue Reference
const T&
Const Lvalue Reference
T&&
Rvalue Reference
const T& is the "universal receiver" - it accepts both lvalues and rvalues!
The "Named Rvalue Reference" Gotcha
Here's one of the most confusing aspects of C++ move semantics, and it trips up even experienced programmers: a named rvalue reference is itself an lvalue!
Let that sink in. Even though you declare a variable as int&& x, once x
has a name, it becomes an lvalue. Why? Because the fundamental rule still applies: if something has
a name and persists across multiple statements, it's an lvalue. The && only describes
what kind of value x can bind to initially, not what x itself is.
This means if you receive an rvalue reference parameter in a function and try to pass it to another
function, it won't bind to that function's rvalue reference parameter - you'll accidentally call the
lvalue version instead! The solution is to use std::move() to cast it back to an rvalue.
#include <iostream>
#include <string>
void takeRvalue(std::string&& str) {
std::cout << "Got rvalue: " << str << std::endl;
}
void forward(std::string&& str) {
// str is a NAMED variable, so it's an lvalue!
// takeRvalue(str); // ERROR: str is lvalue
// Must cast back to rvalue to forward
takeRvalue(std::move(str)); // OK: std::move casts to rvalue
}
int main() {
forward(std::string("Hello"));
return 0;
}
std::move()!
Practice Questions: Rvalue References
Given:
std::string concatenate(const std::string& a, const std::string& b) {
return a + " " + b;
}
Task: Use an rvalue reference to capture the temporary returned by concatenate("Hello", "World"), then modify the captured string by appending "!" and print the result.
Expected output: Hello World!
Show Solution
#include <iostream>
#include <string>
std::string concatenate(const std::string& a, const std::string& b) {
return a + " " + b;
}
int main() {
// Rvalue reference extends temporary lifetime
std::string&& result = concatenate("Hello", "World");
// Can modify through rvalue reference!
result += "!";
std::cout << result << std::endl; // Hello World!
}
Given:
std::vector<int> data = {1, 2, 3};
std::vector<int> createVector() { return {4, 5, 6}; }
Task: Write two overloaded functions named process: one taking std::vector<int>& that prints "Processing lvalue", and one taking std::vector<int>&&& that prints "Processing rvalue". Test with both data and createVector().
Expected output: Processing lvalue then Processing rvalue
Show Solution
#include <iostream>
#include <vector>
void process(std::vector<int>&) {
std::cout << "Processing lvalue" << std::endl;
}
void process(std::vector<int>&&&) {
std::cout << "Processing rvalue" << std::endl;
}
std::vector<int> createVector() { return {4, 5, 6}; }
int main() {
std::vector<int> data = {1, 2, 3};
process(data); // Processing lvalue
process(createVector()); // Processing rvalue
}
Given:
void consume(std::string& s) { std::cout << "lvalue: " << s; }
void consume(std::string&& s) { std::cout << "rvalue: " << s; }
Task: Write a function relay(std::string&& msg) that receives an rvalue reference and forwards it to consume as an rvalue (not as an lvalue). Then call relay with a temporary string "Hello".
Expected output: rvalue: Hello
Hint: Named rvalue references are lvalues. Use std::move() to cast back.
Show Solution
#include <iostream>
#include <string>
#include <utility>
void consume(std::string& s) { std::cout << "lvalue: " << s; }
void consume(std::string&& s) { std::cout << "rvalue: " << s; }
void relay(std::string&& msg) {
// msg is named, so it's an lvalue!
// Must use std::move to forward as rvalue
consume(std::move(msg));
}
int main() {
relay(std::string("Hello")); // rvalue: Hello
}
Move Semantics
Move semantics allow you to transfer resources from one object to another instead of copying. This dramatically improves performance when working with objects that manage resources like memory.
The Problem with Copying
Before C++11, when you passed objects around or returned them from functions, C++ would copy the entire object. For simple types like integers, this is fine. But for objects that manage resources (like memory, file handles, or network connections), copying can be very expensive!
Consider a class that manages a dynamic array. Every time we copy it, we must:
- Allocate new memory (expensive!)
- Copy every single element from the old array to the new one (expensive!)
- Keep both the original and the copy alive
But here's the key insight: if we're copying from a temporary object that's about to be destroyed anyway, why copy at all? We could just "steal" its resources instead! This is exactly what move semantics does.
#include <iostream>
#include <algorithm>
class Buffer {
public:
Buffer(size_t size) : size_(size), data_(new int[size]) {
std::cout << "Constructor: allocated " << size << " ints" << std::endl;
}
// Copy constructor - EXPENSIVE!
Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copy constructor: copied " << size_ << " ints" << std::endl;
}
~Buffer() {
delete[] data_;
std::cout << "Destructor: freed memory" << std::endl;
}
private:
size_t size_;
int* data_;
};
Buffer createBuffer() {
Buffer temp(1000000); // 1 million ints
return temp;
}
int main() {
Buffer b = createBuffer(); // Copy constructor called (without move semantics)
return 0;
}
Move Constructor and Move Assignment
A move constructor "steals" resources from a temporary instead of copying. It takes an rvalue reference parameter and transfers ownership.
#include <iostream>
#include <algorithm>
#include <utility>
class Buffer {
public:
Buffer(size_t size) : size_(size), data_(new int[size]) {
std::cout << "Constructor: allocated " << size << " ints" << std::endl;
}
private:
size_t size_;
int* data_;
};
The Buffer class manages dynamically allocated memory for an array of integers. The constructor
takes a size parameter, allocates memory on the heap using new int[size], and stores
both the size and pointer in member variables. This is a typical resource-owning class that will
benefit from move semantics. Notice we're printing allocation messages to help us see when resources
are allocated, copied, or moved.
// Copy constructor - for lvalues
Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copy constructor: copied " << size_ << " ints" << std::endl;
}
When you copy a Buffer object, the copy constructor must allocate new memory and copy every single
integer from the source buffer to the destination. For a buffer with 1 million integers, that means
allocating 4MB of memory and copying 4MB of data - a very slow operation! This is necessary when
copying lvalues because the original object must remain unchanged. The const Buffer&
parameter ensures we can't modify the source during copying.
// MOVE constructor - for rvalues (temporaries)
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
// Steal the resources
other.size_ = 0;
other.data_ = nullptr; // Leave source in valid state
std::cout << "Move constructor: MOVED " << size_ << " ints (no copy!)" << std::endl;
}
The magic happens here! Instead of allocating new memory and copying data, the move constructor
simply "steals" the pointer from the source object. We take ownership of other.data_
by copying the pointer value (just 8 bytes on 64-bit systems), then set the source's pointer to
nullptr. This prevents the source from deleting our data when it's destroyed. The entire
operation is just a few pointer assignments - incredibly fast regardless of buffer size! The
noexcept is crucial for STL container optimizations.
// Copy assignment operator
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copy assignment: copied " << size_ << " ints" << std::endl;
}
return *this;
}
Copy assignment is called when you assign one existing Buffer to another using = with
an lvalue on the right side (e.g., b1 = b2;). First, we check for self-assignment to
avoid accidentally deleting our own data. Then we free the existing memory, allocate new memory to
match the source size, and copy all the data over. Like the copy constructor, this is expensive for
large buffers because every element must be copied.
// MOVE assignment operator
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_; // Free existing resource
// Steal from other
size_ = other.size_;
data_ = other.data_;
// Leave other in valid state
other.size_ = 0;
other.data_ = nullptr;
std::cout << "Move assignment: MOVED " << size_ << " ints (no copy!)" << std::endl;
}
return *this;
}
Move assignment is called when assigning a temporary (rvalue) to an existing object. We first free
our current resources (since we're replacing them), then steal the pointer from the source just like
in the move constructor. After stealing, we set the source's members to safe default values. This is
vastly more efficient than copy assignment - we're just swapping a few pointers instead of copying
potentially millions of integers. The noexcept guarantee is important for exception safety.
~Buffer() {
delete[] data_;
if (size_ > 0) {
std::cout << "Destructor: freed " << size_ << " ints" << std::endl;
}
}
The destructor is responsible for freeing the dynamically allocated memory to prevent memory leaks.
It calls delete[] data_; which is safe even if data_ is nullptr
(which happens after an object has been moved from). We check if size_ > 0 before
printing the message to avoid printing "freed 0 ints" for moved-from objects, keeping the output clean.
Buffer createBuffer() {
return Buffer(1000000); // Temporary - move constructor will be used
}
int main() {
Buffer b = createBuffer(); // Move constructor called!
Buffer b2(100);
b2 = createBuffer(); // Move assignment called!
return 0;
}
Now let's see move semantics in action! The createBuffer() function returns a temporary
Buffer object. When we assign it to b, the compiler recognizes it's a temporary (rvalue)
and calls the move constructor instead of the copy constructor - saving us from copying 1 million integers!
Similarly, when we assign the return value of createBuffer() to the existing b2,
the move assignment operator is called. Watch the console output to see the difference!
- Allocate new memory
- Copy every element
- Now have two independent copies
- Time: O(n)
- Copy the pointer (just a few bytes)
- Set source pointer to null
- Resources transferred, source is "empty"
- Time: O(1)
std::move - Enabling Move on Lvalues
Move constructors are called automatically for rvalues. But what if you have an lvalue
that you want to move from? Use std::move() to cast it to an rvalue.
#include <iostream>
#include <string>
#include <vector>
#include <utility>
int main() {
std::string str1 = "Hello, World!";
// Copy - str1 still valid
std::string str2 = str1;
std::cout << "After copy: str1 = '" << str1 << "'" << std::endl;
// Move - str1 is now in "moved-from" state
std::string str3 = std::move(str1);
std::cout << "After move: str1 = '" << str1 << "'" << std::endl; // Likely empty
std::cout << "str3 = '" << str3 << "'" << std::endl;
// Vector example
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = std::move(v1);
std::cout << "v1 size after move: " << v1.size() << std::endl; // 0
std::cout << "v2 size after move: " << v2.size() << std::endl; // 5
return 0;
}
// Output:
// After copy: str1 = 'Hello, World!'
// After move: str1 = ''
// str3 = 'Hello, World!'
// v1 size after move: 0
// v2 size after move: 5
When to Use std::move
Now that you understand what std::move() does (casts to rvalue), the natural question
is: when should you use it? The answer requires careful thought, because using std::move()
incorrectly can lead to subtle bugs where your objects are left in unexpected states.
The golden rule is: only use std::move() when you're truly done with the object and you're okay with it being left empty or in a "moved-from" state. After you move from an object, you shouldn't use it again (except to assign it a new value or let it be destroyed).
- When passing to a sink function that will own the data
- When you're done with an object and want to transfer it
- When pushing to containers and you don't need the original
- When implementing move constructor/assignment
- On return statements (prevents RVO/NRVO)
- On const objects (will silently copy instead)
- When you still need the object afterward
- On trivially copyable types (int, double, etc.)
#include <iostream>
#include <string>
#include <vector>
void takeOwnership(std::string s) {
std::cout << "Received: " << s << std::endl;
}
This function takes a std::string by value, which means it will take ownership of the
string passed to it. When we pass an rvalue (like a temporary or a moved-from object), the string's
move constructor is automatically called - no copy happens! This is a "sink" function that consumes
its parameter, making it a perfect candidate for accepting moved-from objects.
std::string createString() {
std::string result = "Hello";
return result; // DON'T use std::move here! RVO will handle it
// return std::move(result); // WRONG - prevents optimization
}
This demonstrates a common mistake: using std::move on return statements. Modern compilers
perform Return Value Optimization (RVO) or Named Return Value Optimization (NRVO), which eliminates the
copy entirely - not even a move! When you add std::move, you actually prevent this optimization
and force a move that wouldn't have been necessary. Let the compiler optimize; trust the return value optimization!
int main() {
std::string name = "Alice";
// Good: we're done with name
takeOwnership(std::move(name));
// Don't use name anymore!
// Good: moving into container
std::vector<std::string> names;
std::string newName = "Bob";
names.push_back(std::move(newName)); // Efficient!
Here we see two good uses of std::move. First, we move name into the sink function -
we're explicitly saying "I'm done with this variable, take it." Second, we move newName into the
vector using push_back. This avoids copying the string and instead transfers its internal buffer
directly to the vector's storage. Both cases follow the golden rule: we don't use these variables after moving from them.
// Bad: const prevents actual move
const std::string constant = "Immutable";
std::string copy = std::move(constant); // Actually copies!
return 0;
}
This demonstrates a subtle trap: moving from const objects. Move semantics requires modifying the source object
(setting its internal pointer to null, for example), but const objects can't be modified! So even though we
use std::move, the compiler silently falls back to calling the copy constructor instead. The code
compiles without error but performs an expensive copy, completely defeating the purpose of using std::move.
The Rule of Five
In C++ programming, there's an important guideline called the Rule of Five. It states: if your class needs to define any one of the following special member functions, you probably need to define all five of them.
Why? If your class manages a resource (like dynamically allocated memory, file handles, or network connections), you need to carefully control what happens when objects are created, copied, moved, assigned, and destroyed. Defining just one or two of these functions usually means your class needs special handling for all of them.
The five special member functions are:
class Resource {
public:
// 1. Destructor
~Resource();
// 2. Copy constructor
Resource(const Resource& other);
// 3. Copy assignment
Resource& operator=(const Resource& other);
// 4. Move constructor
Resource(Resource&& other) noexcept;
// 5. Move assignment
Resource& operator=(Resource&& other) noexcept;
};
noexcept! STL containers like
std::vector will only use move operations during reallocation if they're
guaranteed not to throw.
Practice Questions: Move Semantics
Given:
std::string original = "This is a very long string that we want to move";
Task: Transfer the string to a new variable moved using std::move. Print both strings and their sizes to demonstrate the move occurred.
Expected output: original should be empty (size 0), moved should contain the string.
Show Solution
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string original = "This is a very long string that we want to move";
// Move the string
std::string moved = std::move(original);
std::cout << "original: '" << original << "' (size: "
<< original.size() << ")" << std::endl;
std::cout << "moved: '" << moved << "' (size: "
<< moved.size() << ")" << std::endl;
// Output:
// original: '' (size: 0)
// moved: 'This is a very long string...' (size: 47)
}
Given:
class IntBuffer {
int* data_;
size_t size_;
public:
IntBuffer(size_t n) : data_(new int[n]), size_(n) {
std::cout << "Allocated " << n << " ints" << std::endl;
}
~IntBuffer() {
delete[] data_;
std::cout << "Freed memory" << std::endl;
}
size_t size() const { return size_; }
// TODO: Add move constructor
};
Task: Add a move constructor that steals the pointer from the source object. Test by creating a buffer and moving it to another variable. Verify only one allocation and one deallocation occurs.
Expected output: "Allocated" once, "Freed" once (not twice).
Show Solution
#include <iostream>
#include <utility>
class IntBuffer {
int* data_;
size_t size_;
public:
IntBuffer(size_t n) : data_(new int[n]), size_(n) {
std::cout << "Allocated " << n << " ints" << std::endl;
}
// Move constructor - steal the pointer!
IntBuffer(IntBuffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // Prevent double delete
other.size_ = 0;
std::cout << "Moved (no allocation)" << std::endl;
}
~IntBuffer() {
if (data_) {
delete[] data_;
std::cout << "Freed memory" << std::endl;
}
}
size_t size() const { return size_; }
};
int main() {
IntBuffer buf1(1000);
IntBuffer buf2 = std::move(buf1); // Move, not copy!
std::cout << "buf1 size: " << buf1.size() << std::endl; // 0
std::cout << "buf2 size: " << buf2.size() << std::endl; // 1000
}
Given:
class UniqueString {
char* data_;
size_t length_;
public:
UniqueString(const char* str) {
length_ = strlen(str);
data_ = new char[length_ + 1];
strcpy(data_, str);
}
const char* c_str() const { return data_; }
// TODO: Implement Rule of Five
};
Task: Implement all five special member functions: destructor, copy constructor, copy assignment, move constructor, and move assignment operator. Test by creating, copying, and moving UniqueString objects.
Hint: Use copy-and-swap idiom for exception safety.
Show Solution
#include <iostream>
#include <cstring>
#include <utility>
class UniqueString {
char* data_;
size_t length_;
public:
// Constructor
UniqueString(const char* str) {
length_ = strlen(str);
data_ = new char[length_ + 1];
strcpy(data_, str);
}
// 1. Destructor
~UniqueString() { delete[] data_; }
// 2. Copy constructor
UniqueString(const UniqueString& other)
: length_(other.length_), data_(new char[other.length_ + 1]) {
strcpy(data_, other.data_);
}
// 3. Move constructor
UniqueString(UniqueString&& other) noexcept
: data_(other.data_), length_(other.length_) {
other.data_ = nullptr;
other.length_ = 0;
}
// 4 & 5. Unified assignment (copy-and-swap)
UniqueString& operator=(UniqueString other) noexcept {
std::swap(data_, other.data_);
std::swap(length_, other.length_);
return *this;
}
const char* c_str() const { return data_ ? data_ : ""; }
};
int main() {
UniqueString s1("Hello");
UniqueString s2 = s1; // Copy
UniqueString s3 = std::move(s1); // Move
s2 = UniqueString("World"); // Move assign
std::cout << "s1: " << s1.c_str() << std::endl; // empty
std::cout << "s2: " << s2.c_str() << std::endl; // World
std::cout << "s3: " << s3.c_str() << std::endl; // Hello
}
Perfect Forwarding
Perfect forwarding preserves the value category (lvalue or rvalue) of arguments when passing them to other functions. This is essential for writing generic wrapper functions and factories.
The Problem: Losing Value Category
Imagine you want to write a wrapper function that calls another function with some arguments. You want your wrapper to pass those arguments exactly as they were received - if an lvalue came in, pass it as an lvalue; if an rvalue came in, pass it as an rvalue.
But there's a problem: parameters with names are always lvalues, even if
they're declared as T&&! This is because any named variable has an identity and
a location in memory, making it an lvalue by definition.
This means if you receive an rvalue reference parameter, it becomes an lvalue inside your function, and you'll accidentally trigger copies instead of moves when passing it along. This defeats the purpose of move semantics!
#include <iostream>
#include <string>
void process(std::string& s) { std::cout << "lvalue\n"; }
void process(std::string&& s) { std::cout << "rvalue\n"; }
We set up two overloads of process - one for lvalues taking std::string& and one
for rvalues taking std::string&&. These will help us visualize which version gets called when
we pass arguments through our wrapper function. The key issue we're about to see is that even when we receive
an rvalue in the wrapper, we lose that information when passing it along.
// Naive wrapper - BROKEN!
void wrapper(std::string&& arg) {
// arg is a named variable, so it's an lvalue!
process(arg); // Always calls lvalue version
}
This wrapper function accepts an rvalue reference parameter arg, which seems correct. However,
there's a critical problem: even though arg is declared as std::string&&, once it
has a name, it becomes an lvalue! This is because arg persists throughout the function body and
has a memory location. When we call process(arg), the lvalue version is always selected, defeating
the purpose of having the rvalue reference in the first place.
int main() {
std::string s = "hello";
// wrapper(s); // Won't compile - can't bind lvalue to &&
wrapper(std::string("temp")); // Calls lvalue version - WRONG!
return 0;
}
// Output: lvalue (but we wanted rvalue!)
The test demonstrates the problem: we pass a temporary string std::string("temp") (an rvalue)
to the wrapper, expecting it to forward as an rvalue and call the rvalue version of process.
Instead, the output shows "lvalue" - the wrapper accidentally converted our rvalue to an lvalue! This is the
fundamental problem that perfect forwarding solves. Note that we can't even pass s (an lvalue)
because the wrapper only accepts rvalue references.
Forwarding References (Universal References)
Here's where C++ gets clever. There's a special kind of reference called a forwarding
reference (also known as a universal reference in older C++ literature).
It looks like an rvalue reference (T&&), but it has magical properties!
A forwarding reference can bind to both lvalues and rvalues. But there's a
catch: it only works this way when T is a template parameter being deduced
in that specific context. The form must be exactly T&& with no qualifiers.
Think of it as a "universal adapter" - it accepts any type of value and remembers what kind it was (lvalue or rvalue). This is the foundation of perfect forwarding.
- T is a template parameter being deduced in that context
- The form is exactly
T&&(notconst T&&orvector<T>&&)
#include <iostream>
// T&& here is a FORWARDING REFERENCE
template<typename T>
void universal(T&& arg) {
// T is deduced, so T&& can bind to anything
}
// NOT a forwarding reference - T is not being deduced here
template<typename T>
class Container {
void add(T&& item); // This is just an rvalue reference to T
};
Here we see the crucial distinction: universal uses a forwarding reference because T
is being deduced at the call site, so T&& can bind to both lvalues and rvalues. In contrast,
Container::add does NOT use a forwarding reference - T is fixed when the Container
is instantiated, so T&& is just a regular rvalue reference. The magic only happens when template
type deduction occurs for that specific parameter.
int main() {
int x = 10;
universal(x); // T = int&, T&& = int& (lvalue)
universal(42); // T = int, T&& = int&& (rvalue)
// Reference collapsing rules:
// T& & -> T&
// T& && -> T&
// T&& & -> T&
// T&& && -> T&&
return 0;
}
The magic unfolds: when we pass the lvalue x, the compiler deduces T = int&, so
T&& becomes int& &&, which collapses to int& - an lvalue reference!
When we pass the rvalue 42, the compiler deduces T = int, so T&& is
int&& - an rvalue reference. The reference collapsing rules shown in the comments explain how
double references collapse: any reference with an & collapses to &, and only
&& && stays as &&.
Reference Collapsing Rules
You might be wondering: how can T&& bind to both lvalues and rvalues? The answer
lies in a subtle C++ mechanism called reference collapsing.
Normally, you can't have a "reference to a reference" in C++ - syntax like int& &
is illegal. However, during template type deduction, the compiler might temporarily create such
types internally, and it needs rules to "collapse" them into a single reference type.
Here's how it works: when you pass an lvalue to a forwarding reference template, the template
parameter T deduces to a reference type (e.g., int&). Then when you
apply && to that, you get int& &&, which collapses to int&.
This is why forwarding references can accept lvalues!
When references combine (like T& &&), C++ uses reference collapsing rules:
| If T is... | T&& becomes... | Result |
|---|---|---|
int |
int&& |
rvalue ref |
int& |
int& && = int& |
lvalue ref |
int&& |
int&& && = int&& |
rvalue ref |
& in it collapses to &.
Only && && stays as &&.
std::forward - The Perfect Forwarding Tool
Now we come to the star of perfect forwarding: std::forward. While
std::move unconditionally casts to an rvalue, std::forward is
conditional - it only casts to an rvalue if the original argument was an rvalue.
Here's how it works: std::forward<T>(arg) looks at the template parameter
T to determine what the original argument type was. If T is a
reference type (meaning an lvalue was passed), std::forward returns an lvalue
reference. If T is not a reference (meaning an rvalue was passed), it returns
an rvalue reference.
In other words, std::forward preserves the original value category:
- If the original was an lvalue, forward returns an lvalue reference
- If the original was an rvalue, forward returns an rvalue reference
#include <iostream>
#include <string>
#include <utility>
void process(const std::string& s) {
std::cout << "lvalue: " << s << std::endl;
}
void process(std::string&& s) {
std::cout << "rvalue: " << s << std::endl;
}
We define our test bed with two overloads: one for lvalues taking const std::string& and one for
rvalues taking std::string&&. These will demonstrate whether std::forward correctly
preserves the value category of arguments passed through the wrapper. When the wrapper forwards an lvalue, we
should see "lvalue" printed; when it forwards an rvalue, we should see "rvalue".
// Perfect forwarding wrapper
template<typename T>
void wrapper(T&& arg) {
// std::forward preserves the original value category
process(std::forward<T>(arg));
}
This is the perfect forwarding pattern! The wrapper function takes a forwarding reference T&&,
which can bind to both lvalues and rvalues. The key is std::forward<T>(arg) - it examines
the template parameter T to determine what the original argument was. If T is a
reference type (lvalue was passed), forward returns an lvalue reference. If T is not a reference
(rvalue was passed), forward returns an rvalue reference. This conditional casting preserves the original value category.
int main() {
std::string s = "hello";
wrapper(s); // Forwards as lvalue
wrapper(std::string("world")); // Forwards as rvalue
wrapper("literal"); // Forwards as rvalue (decays to temp)
return 0;
}
// Output:
// lvalue: hello
// rvalue: world
// rvalue: literal
Perfect forwarding in action! When we pass s (an lvalue), the wrapper forwards it as an lvalue,
calling the lvalue version of process. When we pass std::string("world") (an explicit
temporary) or "literal" (which creates a temporary), the wrapper forwards them as rvalues. The
output confirms that std::forward perfectly preserved each argument's value category - exactly what
we need for efficient generic code!
std::move vs std::forward
std::move(arg)
Always casts to rvalue reference
- Use when you know you want to move
- No template parameter needed
- Unconditional cast to rvalue
std::forward<T>(arg)
Conditionally casts based on T
- Use in templates with forwarding refs
- Requires template parameter T
- Preserves original value category
Real-World Example: Factory Function
#include <iostream>
#include <memory>
#include <utility>
#include <string>
class Widget {
public:
Widget(int id, std::string name)
: id_(id), name_(std::move(name)) {
std::cout << "Widget created: " << name_ << std::endl;
}
private:
int id_;
std::string name_;
};
The Widget class demonstrates a typical resource-owning object that benefits from move semantics. Its constructor
takes a std::string by value and uses std::move to transfer it to the member variable
name_. This idiom allows the constructor to accept both lvalues (which will be copied into the parameter)
and rvalues (which will be moved into the parameter), then always moves from the parameter to the member.
// Perfect forwarding factory function
template<typename T, typename... Args>
std::unique_ptr<T> make_unique_custom(Args&&... args) {
return std::unique_ptr<T>(
new T(std::forward<Args>(args)...)
);
}
This is a classic perfect forwarding factory pattern (similar to std::make_unique). The variadic
template Args&&... accepts any number of arguments of any type, and std::forward<Args>(args)...
perfectly forwards each argument to the constructor of T. The ellipsis ... expands the
parameter pack, applying std::forward to each argument individually. This allows the factory to construct
objects with maximum efficiency, copying when necessary and moving when possible.
int main() {
std::string name = "MyWidget";
// name is forwarded as lvalue (will be copied in Widget)
auto w1 = make_unique_custom<Widget>(1, name);
std::cout << "name after w1: " << name << std::endl;
// std::move(name) is forwarded as rvalue (will be moved in Widget)
auto w2 = make_unique_custom<Widget>(2, std::move(name));
std::cout << "name after w2: " << name << std::endl; // Empty!
// Literal is forwarded as rvalue
auto w3 = make_unique_custom<Widget>(3, "Temporary");
return 0;
}
// Output:
// Widget created: MyWidget
// name after w1: MyWidget
// Widget created: MyWidget
// name after w2:
// Widget created: Temporary
The test demonstrates perfect forwarding's efficiency. When we pass name directly, it's forwarded as
an lvalue and copied into the Widget - name remains valid afterward. When we pass std::move(name),
it's forwarded as an rvalue and moved into the Widget - name is left empty. The string literal creates
a temporary and is also forwarded as an rvalue. Perfect forwarding ensures each argument is handled optimally: copies
when we need the original, moves when we don't.
Common Perfect Forwarding Patterns
// Pattern 1: Wrapper function
template<typename Func, typename... Args>
auto invoke_and_log(Func&& f, Args&&... args) {
std::cout << "Calling function..." << std::endl;
return std::forward<Func>(f)(std::forward<Args>(args)...);
}
The wrapper function pattern is used when you want to intercept function calls (for logging, timing, caching, etc.)
while preserving the exact behavior of the original call. Here, invoke_and_log forwards both the
function object f and all its arguments args using perfect forwarding. This ensures that
lvalue arguments remain lvalues and rvalue arguments remain rvalues when passed to the actual function, maintaining
optimal performance and correct semantics.
// Pattern 2: Emplace-style function
template<typename T>
class Container {
std::vector<T> data_;
public:
template<typename... Args>
void emplace(Args&&... args) {
data_.emplace_back(std::forward<Args>(args)...);
}
};
The emplace pattern constructs objects directly in the container's storage, avoiding unnecessary copies or moves.
Instead of creating a temporary T and then moving it into the container, emplace forwards
the constructor arguments directly to the container's internal storage where the object is constructed in-place.
This is exactly how std::vector::emplace_back and similar STL container methods work, providing maximum
efficiency when adding elements.
// Pattern 3: Constructor forwarding
class MyClass {
std::string value_;
public:
template<typename T>
explicit MyClass(T&& val)
: value_(std::forward<T>(val)) {}
};
The constructor forwarding pattern allows a class to accept any type that's convertible to its member type while
preserving move semantics. The forwarding reference T&& accepts both lvalues and rvalues of compatible
types, and std::forward<T>(val) ensures the argument is moved when possible (rvalue) or copied
when necessary (lvalue). This single templated constructor handles all cases efficiently without needing separate
copy and move constructors for the wrapping class.
Practice Questions: Perfect Forwarding
Given:
void target(int& x) { std::cout << "lvalue: " << x << std::endl; }
void target(int&& x) { std::cout << "rvalue: " << x << std::endl; }
Task: Write a template function wrapper that uses perfect forwarding to call target, preserving the value category of its argument. Test with both an lvalue and an rvalue.
Expected output: lvalue: 10 then rvalue: 20
Show Solution
#include <iostream>
#include <utility>
void target(int& x) { std::cout << "lvalue: " << x << std::endl; }
void target(int&& x) { std::cout << "rvalue: " << x << std::endl; }
template<typename T>
void wrapper(T&& arg) {
target(std::forward<T>(arg));
}
int main() {
int x = 10;
wrapper(x); // lvalue: 10
wrapper(20); // rvalue: 20
}
Given:
class Widget {
public:
Widget(int id, const std::string& name) {
std::cout << "Widget(" << id << ", " << name << ")" << std::endl;
}
Widget(int id, std::string&& name) {
std::cout << "Widget(" << id << ", moved " << name << ")" << std::endl;
}
};
Task: Write a variadic template factory function createWidget that perfectly forwards all arguments to the Widget constructor. Test with both lvalue and rvalue string arguments.
Expected: Calling with lvalue string should copy, calling with rvalue should move.
Show Solution
#include <iostream>
#include <string>
#include <utility>
class Widget {
public:
Widget(int id, const std::string& name) {
std::cout << "Widget(" << id << ", " << name << ")" << std::endl;
}
Widget(int id, std::string&& name) {
std::cout << "Widget(" << id << ", moved " << name << ")" << std::endl;
}
};
template<typename... Args>
Widget createWidget(Args&&... args) {
return Widget(std::forward<Args>(args)...);
}
int main() {
std::string name = "Gadget";
auto w1 = createWidget(1, name); // Copies
auto w2 = createWidget(2, std::string("Gizmo")); // Moves
// Output:
// Widget(1, Gadget)
// Widget(2, moved Gizmo)
}
Given:
struct Person {
std::string name;
int age;
Person(std::string n, int a) : name(std::move(n)), age(a) {
std::cout << "Constructed: " << name << std::endl;
}
};
class PersonList {
std::vector<Person> people_;
public:
// TODO: Add emplace method
void print() const {
for (const auto& p : people_)
std::cout << p.name << " (" << p.age << ")" << std::endl;
}
};
Task: Add an emplace method to PersonList that constructs a Person in-place using perfect forwarding. Avoid creating temporary Person objects.
Hint: Use variadic templates with std::forward and emplace_back.
Show Solution
#include <iostream>
#include <string>
#include <vector>
#include <utility>
struct Person {
std::string name;
int age;
Person(std::string n, int a) : name(std::move(n)), age(a) {
std::cout << "Constructed: " << name << std::endl;
}
};
class PersonList {
std::vector<Person> people_;
public:
template<typename... Args>
void emplace(Args&&... args) {
people_.emplace_back(std::forward<Args>(args)...);
}
void print() const {
for (const auto& p : people_)
std::cout << p.name << " (" << p.age << ")" << std::endl;
}
};
int main() {
PersonList list;
std::string name = "Alice";
list.emplace(name, 30); // Copies name
list.emplace("Bob", 25); // Constructs in-place
list.emplace(std::move(name), 35); // Moves name
list.print();
}
Interactive Demo: Value Category Classifier
Test your understanding! Enter a C++ expression and see if you can correctly identify its value category before revealing the answer.
Given: int x = 10;
Key Takeaways
Lvalues Have Identity
Lvalues have names and addresses you can take. Rvalues are temporaries about to be destroyed
&& Binds to Temporaries
Rvalue references (&&) can bind to rvalues, enabling you to detect temporary objects
Move Instead of Copy
Move constructors transfer resources, leaving the source in a valid but empty state
std::move Enables Moving
Use std::move() to cast an lvalue to an rvalue, allowing move operations on it
std::forward Preserves
Use std::forward in templates to preserve the original value category of arguments
Performance Gains
Move semantics can provide massive speedups, especially for containers and large objects
Knowledge Check
Quick Quiz
Test what you've learned about rvalue references and move semantics