AlgoMaster Logo

Rvalue References (&&)

High Priority21 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

The previous lesson introduced move semantics as a way to transfer contents instead of copying them. This chapter covers the language machinery that makes that work: the T&& syntax called an rvalue reference, the three value categories the compiler tracks for every expression, and the overload resolution rules that decide which constructor or assignment the code actually calls. It also revisits std::move and shows it is not a special operation, but a one-line static_cast. Forwarding references look identical (T&&) but only appear in deduced contexts and behave differently, which is the job of a later lesson.

Value Categories: lvalue, prvalue, xvalue

Before C++11, the compiler classified expressions into two buckets: "lvalues" (things with an address that could be taken) and "rvalues" (everything else, mostly temporaries and literals). C++11 split that into three categories because rvalue references introduced a third case the old model could not describe cleanly. The official names are mouthfuls but the underlying idea is simple: every expression is classified by two independent questions, "does it have a stable identity?" and "can its resources be safely stolen?"

CategoryHas identity?Can be moved from?Examples
lvalueYesNoA named variable like cart, obj.field, *ptr, arr[i]
prvalueNoYes42, 3.14, std::string("hi"), Cart(), return-by-value temporaries
xvalueYesYesstd::move(cart), static_cast<T&&>(cart), a function returning T&&

"lvalue" originally meant "appears on the left of an assignment" and "rvalue" meant "appears on the right." Those names have stuck even though the meaning has shifted. A modern, more accurate read: lvalues have a name that can be referred to again later, rvalues are about to disappear so their resources can be taken. xvalues are the bridge: named lvalues explicitly marked as expiring.

The umbrella term rvalue still exists and covers both prvalues and xvalues. When a function takes T&&, it accepts any rvalue, whether prvalue or xvalue. When it takes T&, it accepts only lvalues. That distinction makes overload resolution work.

The diagram shows the two crosscutting questions and how the categories fall out. A glvalue is any expression with identity (lvalue or xvalue). An rvalue is any expression safe to move from (xvalue or prvalue). The xvalue is the overlap: an expression that has both a name and a license to be moved from, which is what std::move produces.

A concrete example makes this less abstract:

Every expression in that program has a category. "Priya" is a prvalue (a string literal that materializes a const char[6], which is an lvalue in this case because string literals are special, but std::string("Priya") would be a prvalue). customer is an lvalue because it is a name. makeName() is a prvalue because it returns by value. std::move(customer) is an xvalue because std::move casts to T&& and the result of such a cast is, by definition, an xvalue.

These categories matter because they decide which overload of a function the compiler picks, and whether a move or a copy actually happens. Get the categories right and the rest follows.

The T&& Syntax and What It Binds To

An rvalue reference is declared with two ampersands: T&&. It is a reference type, like T&, but it binds only to rvalues. The single & reference, by contrast, binds only to lvalues. This is the lever that makes move semantics possible: one constructor accepts lvalues (copy) and another accepts rvalues (move), and the compiler picks the right one based on the value category of the argument.

Four calls, four different overloads picked. The compiler walks the value category of each argument and matches it to the most specific reference type available. A modifiable lvalue picks std::string&. A const lvalue cannot bind to a non-const reference, so it picks const std::string&. A prvalue picks std::string&& because && is a tighter match for rvalues than const& would be. An xvalue from std::move also picks std::string&& for the same reason.

There is a precise rule behind this preference. When both const T& and T&& are viable for an rvalue argument, T&& wins because the standard ranks it as a better match. That is the desired result: move constructors are usually faster than copy constructors, so when both are available, the move one should be picked for temporaries.

What is missing: a plain T& overload cannot bind to a prvalue. Removing the std::string&& overload and trying describe(std::string("Headphones")) still compiles, but selects const std::string& because a non-const reference cannot bind to a temporary. That rule predates C++11 and was the original motivation for const T& parameters.

Uncommenting the first call makes g++ reject it with cannot bind non-const lvalue reference of type 'std::string&' to an rvalue of type 'std::string'. The language is saying "modifying a temporary that is about to die is almost certainly a bug, so this will not compile." T&& reverses that decision: it tells the compiler "this is a temporary, and the temporary is wanted precisely because it is a temporary."

Picking the move overload is not automatic for arguments intended to be discarded. To make a function move from a local cart that is no longer needed, write take(std::move(cart)). Otherwise overload resolution sees an lvalue and copies.

A small but important detail: T&& is a reference, not an object. The thing it refers to lives somewhere else, and the reference does not extend its lifetime in the usual way unless it binds to a temporary in initialization (which is its own special rule).

When an rvalue reference is initialized directly with a prvalue, the temporary it binds to gets its lifetime extended to match the reference. So ref is valid for the rest of the enclosing scope. This is one of the few places where references actually affect object lifetime. It is the same rule that already applies to const T&.

A Named Rvalue Reference Is an Lvalue

This is the rule that confuses C++ developers the first time they see it, and it is the entire reason std::move exists. The rule: an expression that is the name of an rvalue reference is itself an lvalue.

Read that again carefully. The variable's type is std::string&&, an rvalue reference, but the expression that variable's name has value category lvalue. Type and value category are independent properties.

The reasoning is safety. If a variable has a name, it can be referred to more than once. Treating "the name of an rvalue reference" as an rvalue would let a function move from the same parameter twice, gutting it on the first call and leaving a hollow object for the second. By making the named reference an lvalue, the language requires writing std::move(x) every time a move is wanted. That std::move is the acknowledgment that the move is intended.

takeOwnership accepts an rvalue reference. Inside the function, name is a variable of type std::string&&. The first construction, Cart cart1(name), uses the bare name and passes an lvalue to the constructor, picking the copy constructor. The second construction wraps name in std::move, turning the expression into an xvalue, and picks the move constructor. Both objects end up with the string "Priya", but the first paid for a copy and the second did not.

If named rvalue references were rvalues, the first call would move from name, leaving it empty for the second call to copy from. The result would be one valid Cart and one with a moved-from string, and the bug would be hidden because both Cart objects would be in a valid state, just with the second one's owner empty. The lvalue rule prevents that whole class of mistake.

The same rule applies to function parameters of type T&&, return values bound to local rvalue references, and anywhere else an rvalue reference is given a name.

Inside redirect, s is an lvalue even though its declared type is std::string&&. Without the std::move, returning s would try to bind an std::string&& return type to an lvalue, which fails to compile. Writing std::move(s) re-applies the cast and produces an xvalue that matches the return type. Whether returning rvalue references is a good idea comes up below (it usually is not).

Overload Resolution Between T&, const T&, and T&&

With three reference types available, overload resolution has more options than it used to. The rules look complex written out, but in practice they are consistent. Two questions decide everything: is the argument an lvalue or an rvalue, and is it const or not?

ArgumentT&const T&T&&
Non-const lvaluePicks this if availableFalls back hereCannot bind
const lvalueCannot bindPicks this if availableCannot bind
Non-const rvalue (prvalue or xvalue)Cannot bindFalls back herePicks this if available
const rvalueCannot bindFalls back hereCannot bind (rare in practice)

In words: non-const lvalues prefer T&, rvalues prefer T&&, and const T& is the universal fallback that accepts almost anything. That last property is why pre-C++11 code uses const T& everywhere; it works for any caller. Post-C++11, add T&& overloads only when something faster can be done with the rvalue, like moving from it.

Four calls, four overloads. The prvalue and xvalue both hit the && overload, because both are rvalues. The compiler does not care which kind of rvalue was passed.

Having all three overloads is overkill for most types. The non-const T& overload is rarely useful, because it lets a caller modify the argument while passing it (the function appears to take, not borrow). In real code, the common patterns are:

PatternWhen
Only const T&Read-only access, no move needed
const T& + T&&You can move when given an rvalue, copy otherwise
Pass by valueThe function will store a copy anyway, let the caller decide via move
Only T&The function is explicitly meant to mutate the caller's object

The "pass by value" approach (sometimes called the sink idiom) is popular because it collapses two overloads into one. The compiler picks copy or move at the call site:

(The program produces no output because the example only stores values.) Either way notes_ ends up holding the string. The lvalue caller pays for one copy plus one move. The rvalue caller pays for two moves. For movable types this is competitive with the two-overload approach and considerably less code. The downside: there is always at least one move on top of whatever the caller paid, so for types that are not move-friendly (or for hot inner loops where a difference has been measured), keeping const T& + T&& overloads is faster.

Pass-by-value-then-move costs one extra move compared to a hand-written T&& overload. Moves on std::string, std::vector, and most STL types are cheap (typically a few pointer swaps), but not zero. Avoid this pattern when optimizing a tight loop.

Returning Rvalue References (Usually Wrong)

A function can return T&&. It almost never should. The temptation is the same as returning T&: "hand back a reference and skip a copy." The bug is the same too: if the referenced object dies before the caller uses it, the result is a dangling reference and undefined behavior.

local is destroyed when makeBroken returns. The rvalue reference the function hands back points at an object that no longer exists. The caller's result is constructed from a dangling reference, which is undefined behavior. The program might appear to print "Order shipped" with optimizations off, garbage with optimizations on, or crash entirely. The compiler typically warns with something like warning: reference to local variable 'local' returned.

The correct version returns by value:

Return by value is correct. Modern compilers apply Named Return Value Optimization (NRVO) and construct local directly in the caller's storage, eliding the move entirely. Even when NRVO cannot apply, the implicit return falls back to a move because the standard treats return local; as if it were return std::move(local); for a local variable of the function's return type. No copy, no dangling reference.

There are two narrow cases where returning T&& is reasonable. The first is std::move itself, which returns an rvalue reference to its argument because the caller already owns the object and the cast does not extend any lifetime. The second is helpers in the standard library like std::forward. Application code is not writing one of those. Return by value.

The function takes a forwarding reference and returns an rvalue reference. The returned reference refers to the caller's object, not to anything inside move, so no dangling occurs.

Reference Qualifiers on Member Functions

C++11 added a feature that pairs with rvalue references: a member function can be qualified so it is callable only on lvalue objects, only on rvalue objects, or both. The syntax puts a & or && after the function signature, like const.

Why would you want this? Two reasons. The first is correctness: some operations make sense only on a named object, not on a temporary. The second is performance: when you know the object is about to die, you can move its internals out instead of copying them.

The first call uses the &-qualified overload, copies the vector, and leaves cart fully populated. The second call uses the &&-qualified overload because makeCart() returns a prvalue, and moves the vector out of the soon-to-die temporary. The compiler picks the right overload purely from the value category of the object expression.

Without reference qualifiers, the class would have one items() function that always copies, and the move opportunity on the prvalue would be lost. Reference qualifiers allow opting into the optimization without breaking the common lvalue case.

A parallel rule catches another bug: assigning to a temporary. Pre-C++11, Cart() = someCart compiled because the default operator= had no reference qualifier. The assignment was useless (the temporary died on the next line), but the language let it through. Modern types can prevent it:

Adding & after operator= says "only callable when *this is an lvalue." The commented line would now fail with passing 'OrderId' as 'this' argument discards qualifiers or a similar message. Pre-C++11 there was no way to express this restriction. It is a small thing, but it eliminates an entire family of "why is my assignment doing nothing?" bugs.

std::move Revisited: It Is Just a Cast

The Move Semantics lesson introduced std::move as "the thing called to enable a move." With value categories in hand, it is now clear what it actually is: a static_cast to T&& wrapped in a template that figures out T. Nothing moves when std::move is called. The cast changes the expression's value category from lvalue to xvalue, which then makes overload resolution pick the move constructor or move assignment.

The output shows that a ended up empty and b got the string, exactly what the real std::move would do. myMove is structurally the standard library's implementation (with noexcept and a couple of details stripped). The "magic" of move semantics is a well-chosen cast plus the language rules around T&& overloads.

Two consequences fall out of this.

First, std::move is free at runtime. The cast does not generate any code. The savings (or costs, when misused) come entirely from which constructor or assignment the compiler subsequently picks. Calling std::move on an int does not speed anything up, because int does not have a "move constructor that is faster than copy." Calling std::move on a std::vector with a million elements saves the allocation and copy of those million elements.

Second, std::move does not move. It signals intent. The actual transfer happens inside the move constructor or move assignment operator of the target type, which has to be written to take advantage of the rvalue. If a type has no move constructor (or it is = deleted), std::move still casts to T&&, the call still tries to bind that rvalue, and the compiler falls back to the copy constructor that takes const T&. A copy happens without warning. That is not a bug, but it looks like a bug when the type was assumed to be moveable.

Receipt has only a copy constructor. std::move(r1) produces a Receipt&& expression. Overload resolution looks for a constructor taking Receipt&&. There is none. It falls back to the copy constructor, which accepts const Receipt& and binds to that rvalue. The cast did not lie, but it did not help either. The fix is to declare a move constructor.

std::move on a moved-from object is not undefined behavior, but the object is in an unspecified valid state. Move-then-move is fine. Use-then-move is fine. Move-then-use is where the trap lives.

T&& in Deduced Contexts Is a Forwarding Reference

Everything in this chapter assumes T is concrete. Putting T&& into a template parameter position where the compiler deduces T from the argument changes its meaning. The same T&& becomes a forwarding reference (sometimes called a universal reference, the older name Scott Meyers coined), and it can bind to lvalues as well as rvalues. Forwarding references rely on reference collapsing rules.

The first signature accepts only rvalues, every call. The second signature accepts anything because T gets deduced differently depending on whether the argument is an lvalue or an rvalue. From a typing perspective they look identical. From a semantics perspective they are very different. The distinction is flagged here so that this chapter does not lead to the conclusion that "T&& always means rvalue." It means rvalue only in non-deduced contexts. The Perfect Forwarding lesson takes the deduced-context case apart properly.

Quiz

Rvalue References Quiz

10 quizzes