Move semantics & rvalue references

RoadmapsC++

Scenario

You hear that `std::move` makes code faster. You change `return my_local_vector;` to `return std::move(my_local_vector);`. When you benchmark the code, it has actually gotten slower.

Why did explicitly telling the compiler to move the object make the code slower?

Mental model

`std::move` is terribly named. It does not move anything. It is just a cast that whispers to the compiler: 'I am done with this variable, you can strip it for parts.'

Move semantics (C++11) allow transferring ownership of resources from temporary objects (rvalues) instead of doing expensive deep copies. The trap is thinking `std::move` is a magic speed button. Overusing it—specifically on local variables being returned from a function—disables the compiler's Native Return Value Optimization (NRVO), forcing a move where the compiler would have done literally zero work.

Explanation

Before C++11, returning a massive `std::vector` by value meant the compiler had to make a full deep copy. Move semantics introduced 'rvalue references' (`&&`). If the compiler knows an object is about to be destroyed (like a temporary return value), it binds it to an rvalue reference. This triggers the Move Constructor, which simply steals the pointers from the temporary object instead of copying the data.

**What does std::move actually do?**

`std::move` does absolutely no work. It is purely a `static_cast` to an rvalue reference (`T&&`). It flags a variable saying, 'I don't need this anymore, treat it as a temporary.' This allows you to forcefully transfer ownership of variables (e.g., passing a `std::unique_ptr` into a vector).

**The NRVO Trap:**

Modern C++ compilers use Return Value Optimization (RVO/NRVO). When you write `return local_vector;`, the compiler is so smart it actually constructs `local_vector` directly in the caller's memory space. Zero copies, zero moves. It is pure teleportation.

If you write `return std::move(local_vector);`, you force the variable to be cast to an rvalue reference. This breaks the compiler's ability to teleport it. Instead, it is forced to run the move constructor. You just turned zero work into non-zero work.

Code examples

The bug: Pessimizing moves

#include <iostream>
#include <vector>

std::vector<int> generateData() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    
    // BUG: Trying to be smart. This disables NRVO.
    // The compiler is forced to move 'data' into the return slot.
    return std::move(data); 
}

int main() {
    std::vector<int> result = generateData();
    return 0;
}

The explicit `std::move` prevents the compiler from constructing `data` directly in `result`'s memory. It creates `data` locally, and then invokes the move constructor to transfer it.

The fix: Trust the compiler (NRVO)

#include <iostream>
#include <vector>

std::vector<int> generateData() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    
    // FIX: Just return it by value.
    // NRVO kicks in: 'data' is constructed exactly where 'result' lives.
    // ZERO copies, ZERO moves.
    return data; 
}

int main() {
    std::vector<int> result = generateData();
    return 0;
}

By returning a local variable directly, modern compilers employ copy elision. It is mathematically the fastest possible way to return an object by value.

Key points

Common mistakes

Recall questions

Questions & answers

A candidate writes `std::vector<int> v1 = {1, 2, 3}; std::vector<int> v2 = std::move(v1); std::cout << v1[0];`. What happens?

The program will likely crash (Segfault). `v1` has been moved from, meaning `v2`'s move constructor stole its heap pointer and left `v1`'s internal pointer as `nullptr`. Attempting to dereference `v1[0]` accesses null memory.

Approach: Understand that 'moved-from' objects are hollowed out.

How do you transfer a `std::unique_ptr` into a function `void process(std::unique_ptr<Data> ptr)`?

You must use `process(std::move(my_ptr));`. Because `unique_ptr` cannot be copied, you must explicitly cast it to an rvalue to invoke its move constructor and transfer ownership into the function.

Approach: Recognize when `std::move` is strictly required to pass uncopyable types.

Continue learning

Previous: Rule of 3 / 5 / 0 - copy, move, destructor

Return to C++ Roadmap