std::move doesn't move. std::forward doesn't forward. At runtime, neither generates a single byte of code. They are casts, and understanding them is the key to writing fast modern C++.
Let's get the shocking truth out of the way: std::move doesn't move anything. std::forward doesn't forward anything. At runtime, they generate zero executable code. They are casts.
c++ // Simplified implementation of std::move template<typename T> decltype(auto) move(T&& param) { using ReturnType = std::remove_reference_t<T>&&; return static_cast<ReturnType>(param); }
std::move unconditionally casts its argument to an rvalue. That's all it does. The cast enables moving by making the argument eligible for move constructors and move assignment operators, but the actual move happens (or doesn't) in the function that receives the rvalue reference.
std::forward is a conditional cast: it casts to rvalue only if its argument was initialized with an rvalue. This is what enables perfect forwarding — passing arguments to another function while preserving their original value category:
c++ template<typename T> void wrapper(T&& arg) { target(std::forward<T>(arg)); // If arg was initialized with rvalue: forwards as rvalue // If arg was initialized with lvalue: forwards as lvalue }
std::move on rvalue references (you know they're rvalues). Use std::forward on universal references (they might be either). Never use std::move on universal references — you might move from an lvalue the caller didn't expect to be modified.std::move actually do at runtime?Not every T&& is an rvalue reference. When type deduction is involved, T&& becomes a universal reference (also called a "forwarding reference") — it can bind to lvalues and rvalues:
c++ // Universal reference: T is deduced template<typename T> void f(T&& param); // universal reference // Rvalue reference: no type deduction on Widget void f(Widget&& param); // rvalue reference // Universal reference: auto deduction auto&& var = expr; // universal reference
T&& (not const T&&, not vector<T>&&). (2) Type deduction must occur for T. If either condition is violated, you have a plain rvalue reference.c++ // NOT universal references (no deduction on &&): void f(const T&& param); // rvalue ref (const) void f(std::vector<T>&& p); // rvalue ref (not T&&) // ALSO NOT universal (push_back's T is already known): template<class T> class vector { void push_back(T&& x); // rvalue ref, T fixed at class instantiation };
void f(const T&& param) NOT a universal reference?The rule is simple: apply std::move to rvalue references (you know they bind to rvalues), and std::forward to universal references (they might be either):
c++ class Widget { public: // rhs is rvalue ref: always use std::move Widget(Widget&& rhs) : name(std::move(rhs.name)) {} // newName is universal ref: use std::forward template<typename T> void setName(T&& newName) { name = std::forward<T>(newName); } };
For functions returning by value, apply std::move to rvalue references and std::forward to universal references being returned. But never apply std::move to local variables being returned — this prevents Return Value Optimization (RVO).
c++ // DO NOT do this: prevents RVO Widget makeWidget() { Widget w; return std::move(w); // BAD: inhibits RVO } // Just return the local: compiler applies RVO or implicit move Widget makeWidget() { Widget w; return w; // GOOD: RVO or implicit move }
std::move a local variable in a return statement?Overloading a function that takes a universal reference is almost always a mistake. The universal reference overload is a greedy template that will match nearly anything:
c++ class Person { public: template<typename T> explicit Person(T&& n) : name(std::forward<T>(n)) {} explicit Person(int idx); // overload for index lookup }; Person p("Nancy"); // OK: calls universal ref ctor Person p(42); // OK: calls int ctor short idx = 42; Person p(idx); // ERROR: calls universal ref ctor! // short matches T&& better than int
int overload requires a conversion from short, so the template wins. It then tries to initialize std::string from a short — compile error.It gets worse with copy/move constructors. If a non-const lvalue of type Person is passed, the universal reference template matches better than the copy constructor (which takes const Person&):
c++ Person p("Nancy"); auto cloneOfP(p); // calls universal ref ctor, NOT copy ctor! // Template deduces Person&, exact match. // Copy ctor takes const Person& (requires adding const).
If you can't overload on universal references safely, what do you do? Meyers presents several alternatives:
c++ // Option 5: enable_if constrains the template class Person { public: template<typename T, typename = std::enable_if_t< !std::is_base_of_v<Person, std::decay_t<T>> && !std::is_integral_v<std::remove_reference_t<T>> >> explicit Person(T&& n) : name(std::forward<T>(n)) {} explicit Person(int idx); };
std::enable_if do in the context of universal reference overloading?C++ forbids references to references in source code, but the compiler creates them internally during template instantiation. The reference collapsing rules resolve them:
| Template Arg | && applied | Collapsed to |
|---|---|---|
| T = Widget& | Widget& && | Widget& (lvalue ref) |
| T = Widget&& | Widget&& && | Widget&& (rvalue ref) |
| T = Widget | Widget && | Widget&& (rvalue ref) |
This is the mechanism that makes universal references and std::forward work. When an lvalue is passed to template<typename T> void f(T&&), T is deduced as Widget&. The parameter type becomes Widget& &&, which collapses to Widget& — an lvalue reference. When an rvalue is passed, T is Widget, and the parameter is Widget&&.
See how lvalues and rvalues flow through a universal reference.
Widget& && collapse to?Move semantics are C++11's marquee performance feature, but there are several situations where moving isn't present, isn't cheap, or isn't used:
| Situation | Why move doesn't help |
|---|---|
| No move operations | Many C++98 legacy types don't have move ctors. "Moving" them falls back to copying. |
| Move not faster | std::array moves are O(n) — same as copy. Small-string optimization (SSO) means short strings copy their data, not just a pointer. |
| Move can't be used | Contexts requiring strong exception guarantees (e.g., vector reallocation) only use move if it's noexcept. |
| Source is lvalue | Only rvalues and xvalues trigger moves. Lvalues are copied unless you explicitly std::move them. |
std::array not faster than copying it?Perfect forwarding fails when template type deduction deduces the wrong type, or can't deduce at all. Here are the main failure cases:
fwd({1, 2, 3}) fails. Templates can't deduce std::initializer_list from braces. Fix: use auto il = {1,2,3}; fwd(il);int, not pointer. Fix: use nullptr (see Item 8).fwd(processWidget) fails if processWidget is overloaded. The template can't know which overload. Fix: cast to the desired function pointer type.fwd({1, 2, 3}) fail with perfect forwarding?Watch how move semantics transfer resources instead of copying them. Click to see how a std::string is moved vs copied.
Click the buttons to see each operation animate.
| Item | Key Takeaway |
|---|---|
| Item 23 | std::move = unconditional cast to rvalue. std::forward = conditional cast. Neither generates runtime code. |
| Item 24 | T&& with type deduction = universal reference. Otherwise = rvalue reference. |
| Item 25 | std::move on rvalue refs, std::forward on universal refs. Never std::move a return value. |
| Item 26 | Don't overload on universal references — they match almost everything. |
| Item 27 | Alternatives: different names, const T&, pass-by-value, tag dispatch, enable_if. |
| Item 28 | Reference collapsing: & + anything = &. Only && + && = &&. |
| Item 29 | Don't assume move is always present, cheap, or used. |
| Item 30 | Perfect forwarding fails for braces, 0/NULL, static const, overloaded names, bitfields. |
"std::move doesn't move anything. std::forward doesn't forward anything. At runtime, neither does anything at all." — Scott Meyers