C++ Module 12 – Move semantics and smart pointers
Forwarding references and perfect forwarding
How T&& in a C++ template binds to any argument, how reference collapsing and std::forward pass each argument on unchanged, and where forwarding breaks.
What you will learn
- Recognise a forwarding reference and tell it from an rvalue reference
- Apply the reference collapsing rules to work out a parameter's type
- Pass arguments on with std::forward so that lvalues are copied and rvalues moved
- Debug forwarding mistakes such as std::move on a forwarding reference
Before you start
What you’ll learn · Before you start
What you will learn
- Recognise a forwarding reference and tell it from an rvalue reference
- Apply the reference collapsing rules to work out a parameter's type
- Pass arguments on with std::forward so that lvalues are copied and rvalues moved
- Debug forwarding mistakes such as std::move on a forwarding reference
Before you start
On this page
A logging factory that takes const std::string& copies every name it is given, even a temporary it could have
moved, and one that takes std::string&& rejects a named string. A
forwarding reference: A reference written auto&&, or T&& in a template whose T is deduced, that binds to anything: it becomes an lvalue reference for a named variable and an rvalue reference for a temporary. More in “C++ auto, decltype and type aliases” accepts both: a parameter written T&&, where T is a
template parameter deduced from the call, as in template <typename T> void f(T&& x), or a variable declared
auto&&. For an lvalue, T is deduced as an lvalue reference, and
reference collapsing: The rule that turns a reference to a reference, formed through a template parameter or an alias, into one reference: an lvalue reference wins whenever one is involved, and only && combined with && stays &&. With T = std::string&, T&& is std::string&. turns T&& into an lvalue reference too; for an rvalue,
T is the plain type and the parameter is an rvalue reference: A reference declared with &&, such as int&& r, that binds to temporaries and other objects about to expire but never to a named variable. Unlike a const reference, it lets you change the object it names. More in “C++ references: lifetime extension and dangling”. Inside the function
the parameter has a name, so it is an lvalue, and std::forward<T>(x) casts it back to the category the caller used.
Doing that for every argument is perfect forwarding: Passing each argument of a wrapper on with the value category the caller gave it, through a forwarding reference and std::forward<T>, so that the wrapped call copies lvalues and moves rvalues exactly as a direct call would.: emplace_back and
std::make_unique hand their arguments to a constructor as if the caller had called it directly, copying lvalues and
moving rvalues.
T&& is a forwarding reference only when T is deduced
The rule is narrow. The parameter must be exactly T&&, with no const, and T must be deduced from this very call.
Forwarding references are the parameters of function templates written like that, and auto&& variables. Everything
else spelled with && is an ordinary rvalue reference that accepts only rvalues:
const T&&is an rvalue reference to const, because theconstbreaks the rule.std::vector<T>&&is an rvalue reference to a vector, because the parameter is notT&&itself.void push(T&& item)insidetemplate <typename T> class Queueis an rvalue reference, becauseTwas fixed when the class was instantiated, not deduced by the call.
Step through the tests with each declaration:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- f(T&& x). template <typename T> void f(T&& x): the parameter is written exactly T&&, T is a template parameter of f, and every call deduces it. It passes all three tests: a forwarding reference.
- auto&& x. A variable declared auto&& is deduced from its initialiser in the same way, so it is a forwarding reference too: auto&& x = make(); binds to the temporary, auto&& y = s; to the named s.
- const T&&. template <typename T> void f(const T&& x): the const breaks the first test. This is an rvalue reference to const, which accepts only rvalues.
- std::vector<T>&&. template <typename T> void f(std::vector<T>&& v): T is deduced, but the parameter is not T&& itself. It is an rvalue reference to a vector, so a named vector does not bind.
- Queue<T>::push. void push(T&& item) in template <typename T> class Queue: T was fixed when Queue<T> was instantiated, so no call deduces it. push accepts only rvalues.
- What T records. For an lvalue argument, a forwarding reference deduces T as an lvalue reference; for an rvalue, as the plain type. The program below prints what T became for four arguments.
The program below calls template <typename T> void show(T&&) with four arguments and prints what T and the
parameter became:
// What T becomes in template <typename T> void show(T&& x), for four kinds of argument, and what T&& collapses to.
#include <print>
#include <string>
#include <utility>
template <typename T> constexpr const char* name = "?";
template <> constexpr const char* name<std::string> = "std::string";
template <> constexpr const char* name<std::string&> = "std::string&";
template <> constexpr const char* name<std::string&&> = "std::string&&";
template <> constexpr const char* name<const std::string> = "const std::string";
template <> constexpr const char* name<const std::string&> = "const std::string&";
template <> constexpr const char* name<const std::string&&> = "const std::string&&";
template <typename T>
void show(const char* call, T&&) {
std::println("{:<26} T = {:<20} parameter = {}", call, name<T>, name<T&&>);
}
std::string make() { return "made"; }
int main() {
std::string s = "text";
const std::string c = "constant";
show("show(s)", s);
show("show(c)", c);
show("show(std::move(s))", std::move(s));
show("show(make())", make());
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra deduce.cpp -o deduce && ./deduce
g++-16 -std=c++23 -Wall -Wextra deduce.cpp -o deduce-gcc
./deduce > clang.txt && ./deduce-gcc > gcc.txt && diff clang.txt gcc.txt && echo "GCC deduces the same"
""") Output
$ clang++ -std=c++23 -Wall -Wextra deduce.cpp -o deduce && ./deduce show(s) T = std::string& parameter = std::string& show(c) T = const std::string& parameter = const std::string& show(std::move(s)) T = std::string parameter = std::string&& show(make()) T = std::string parameter = std::string&& $ g++-16 -std=c++23 -Wall -Wextra deduce.cpp -o deduce-gcc $ ./deduce > clang.txt && ./deduce-gcc > gcc.txt && diff clang.txt gcc.txt && echo "GCC deduces the same" GCC deduces the same
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 deduce_session.py
For the lvalue s, T is std::string&, and the const lvalue c keeps its const in T. For the xvalue
std::move(s) and the prvalue make(), T is plain std::string, and the parameter is std::string&&. That
asymmetry, a reference in T only for lvalues, is how the function can later tell what it was given.
Checkpoint
Check: Recognising a forwarding reference
Reference collapsing gives the parameter its type
T&& with T = std::string& would be a reference to a reference, which C++ does not allow you to write. When such a
type arises through a template parameter or an alias, the standard collapses it into one reference, and an lvalue
reference wins whenever one is involved. In the table, U stands for any type, such as std::string:
T is |
T& becomes |
T&& becomes |
|---|---|---|
U& |
U& |
U& |
U&& |
U& |
U&& |
U |
U& |
U&& |
Only an rvalue reference combined with an rvalue reference stays an rvalue reference. The “parameter” column of the run
above is this table applied to the deduced T: for show(c), T is const std::string&, so T&& collapses to
const std::string&. The same table explains auto&&: in auto&& y = s;, auto is deduced as std::string&, so
y is a std::string& bound to s.
Checkpoint
Check: Reference collapsing
std::forward restores the caller’s value category
std::forward<T>(x) is defined as static_cast<T&&>(x). When T is std::string&, that collapses to
std::string& and the result is an lvalue; when T is std::string, the result is an xvalue that can be moved from,
as the two columns of the diagram show:
How a forwarding reference remembers what the caller passed
Text description of the diagram
The diagram is titled "template <typename T> void f(T&& x)" and has two columns, one for an lvalue argument and one for an rvalue argument. Each column has four steps from top to bottom.
- The call: f(name) on the left, f(std::move(name)) on the right.
- T is deduced as: std::string& on the left, std::string on the right.
- T&& collapses to the parameter type: std::string& on the left, std::string&& on the right.
- std::forward<T>(x) gives: an lvalue, which is copied, on the left; an xvalue, which is moved, on the right.
A note at the bottom says that x, passed on by name, is an lvalue, and that std::forward restores what was passed.
You always write the template argument, because std::forward must not deduce it: the information lives in T, not
in the type of x. A factory that forwards a pack of arguments passes each one on with its own category:
// make_logged<T>(args...) passes its arguments on to T's constructor exactly as it received them, and each
// constructor of Badge prints which one ran.
#include <print>
#include <string>
#include <utility>
struct Badge {
std::string name;
int level = 0;
Badge(const std::string& n) : name(n) { std::println(" Badge(const std::string&): copied the name"); }
Badge(std::string&& n) : name(std::move(n)) { std::println(" Badge(std::string&&): moved the name"); }
Badge(const std::string& n, int l) : name(n), level(l) { std::println(" Badge(const std::string&, int)"); }
};
template <typename T, typename... Args>
T make_logged(Args&&... args) {
std::println("make_logged with {} argument(s)", sizeof...(args));
return T(std::forward<Args>(args)...);
}
int main() {
std::string name = "Asha Rao";
Badge a = make_logged<Badge>(name); // an lvalue: copied
Badge b = make_logged<Badge>(std::move(name)); // an xvalue: moved
Badge c = make_logged<Badge>(std::string("Ravi"));
Badge d = make_logged<Badge>(a.name, 3);
std::println("a = {}, b = {}, c = {}, d = {} (level {}); name is now \"{}\"", a.name, b.name, c.name, d.name, d.level, name);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra make_logged.cpp -o make_logged && ./make_logged
g++-16 -std=c++23 -Wall -Wextra make_logged.cpp -o make_logged-gcc
./make_logged > clang.txt && ./make_logged-gcc > gcc.txt && diff clang.txt gcc.txt && echo "GCC prints the same"
""") Output
$ clang++ -std=c++23 -Wall -Wextra make_logged.cpp -o make_logged && ./make_logged
make_logged with 1 argument(s)
Badge(const std::string&): copied the name
make_logged with 1 argument(s)
Badge(std::string&&): moved the name
make_logged with 1 argument(s)
Badge(std::string&&): moved the name
make_logged with 2 argument(s)
Badge(const std::string&, int)
a = Asha Rao, b = Asha Rao, c = Ravi, d = Asha Rao (level 3); name is now ""
$ g++-16 -std=c++23 -Wall -Wextra make_logged.cpp -o make_logged-gcc
$ ./make_logged > clang.txt && ./make_logged-gcc > gcc.txt && diff clang.txt gcc.txt && echo "GCC prints the same"
GCC prints the same
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 make_logged_session.py
Step through the four calls:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- An lvalue. make_logged<Badge>(name) passes the named string. Args is deduced as std::string&, so the parameter is an lvalue reference to name.
- It arrives as an lvalue. std::forward<std::string&>(args) gives an lvalue, so Badge(const std::string&) runs and copies the name, as a direct Badge(name) would.
- An xvalue. make_logged<Badge>(std::move(name)) passes an xvalue. Args is deduced as plain std::string, so the parameter is std::string&&.
- It arrives as an rvalue. std::forward<std::string>(args) gives an xvalue, so Badge(std::string&&) runs and moves the characters out of name, which ends up empty.
- A temporary. std::string("Ravi") is a prvalue. It binds to the parameter, which is std::string&& again, and reaches the moving constructor in the same way.
- Two arguments. make_logged<Badge>(a.name, 3) forwards a pack of two: an lvalue string and an int. Each keeps its own category, and the two-parameter constructor runs.
- The same constructors. Every call reached the constructor a direct call would have reached. The run ended with a = Asha Rao, b = Asha Rao, c = Ravi, d = Asha Rao (level 3) and an empty name.
make_logged<Badge>(name) reached the constructor that copies, and make_logged<Badge>(std::move(name)) the one that
moves, so name ended up empty, as it would with Badge(std::move(name)) written directly. A temporary went to the
moving constructor, and two arguments went to the two-parameter constructor. Perfect forwarding is exactly that:
make_logged adds a log line and changes nothing about which constructor runs. It is also how emplace_back,
std::make_unique and std::invoke pass on what they receive. Rule F.19 of the C++ Core Guidelines puts it in one
line: pass “forward” parameters as TP&& and only std::forward them.
Key idea
A named parameter is an lvalue whatever the caller passed; std::forward<T> restores the caller’s category from
T, so a wrapper reaches the constructor that a direct call would reach.
Where forwarding goes wrong, and where it is used
std::moveon a forwarding reference moves from the caller’s lvalue too, so the caller’s object is emptied.- Forwarding twice passes an rvalue on twice, so the second receiver gets a moved-from object.
- A greedy forwarding constructor matches a non-const object better than the copy constructor does.
- An overload next to a forwarding template loses arguments of nearby types, such as a
shortpassed to anintoverload, to the template. emplace_backforwards constructor arguments into the vector’s own storage, so no temporary is built first.decltype(auto)makes a call wrapper return exactly the type the wrapped call returns, references included.auto&&in a range-for loop binds to every element type, including the proxy objects ofstd::vector<bool>.- A generic lambda with an
auto&¶meter has a forwarding reference too, but no template parameter to name instd::forward<T>.
Checkpoint
Check: std::forward and perfect forwarding
Practise: deduce, collapse, forward
Questions on deduction, collapsing and std::forward follow, weakest idea first; a missed one returns before the end.
Practice that adapts to you
Practise this lesson
11 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Four forwarding bugs reproduced, with their fixes Pro: opens with a Pro, Premium or Ultimate pass
Forwarding mistakes rarely draw a warning, and their symptoms appear far from the template that caused them. Each of the four below is reproduced on Apple clang 21 and GCC 16, with the change that fixes it.
In this part:
- std::move on a forwarding reference empties the caller’s object
- Forwarding the same argument twice
- A forwarding constructor that beats the copy constructor
- An overload that a forwarding template outbids
8 runnable examples (C++, Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Forwarding in real code: emplace, call wrappers and auto&& Pro: opens with a Pro, Premium or Ultimate pass
Forwarding is at work in code that most C++ programs use daily: emplace_back, call wrappers, range-for loops and
generic lambdas. Each is run below on both compilers, and an exercise builds a factory and a call wrapper that must
keep every category intact.
In this part:
- Building in place against pushing a temporary, counted
- A call wrapper that keeps reference returns
- auto&& in a range-for loop and proxy references
- Forwarding from a generic lambda
- Your turn: a factory and a call wrapper
10 runnable examples (C header, C++, Python) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher to mid · Warm-up
Is every T&& in a template a forwarding reference?
No. T&& is a forwarding reference only when T is a template parameter of the function itself, deduced from the call, and the parameter is written exactly T&&, without const; auto&& variables are forwarding references too. const T&& is an rvalue reference to const, std::vector<T>&& is an rvalue reference to a vector, and T&& in a member function of a class template, such as void push(T&& item) in template <typename T> class Queue, is an ordinary rvalue reference, because T was fixed when the class was instantiated. Only a forwarding reference binds to lvalues as well as rvalues, deducing T as an lvalue reference for an lvalue, and only for it is std::forward<T> the right way to pass the parameter on.
-
Mid to senior
Widget has a constructor template, template <typename T> Widget(T&& name), to accept anything a std::string can be made from. Copying a const Widget works, but Widget b(a); for a non-const a fails to compile with an error about building a std::string from a Widget. Explain why, and fix it two ways.
-
Fresher to mid
A task queue has template <typename Task> void enqueue(Task&& t) { tasks_.push_back(std::move(t)); }. After a refactor, callers find their own task objects empty after enqueue(task). Explain the bug, show the fix, and say when std::move on a parameter like t would be right.
-
Senior to staff
Write timed(f, args...), which calls f with args, logs how long the call took and returns f's result unchanged, including references and void. What must the implementation get right, and how would you test it?
Key takeaways
T&&withTdeduced by the call, andauto&&, are forwarding references: they bind to lvalues and rvalues alike.- For an lvalue argument
Tis an lvalue reference; reference collapsing turnsT&&into an lvalue reference. - A parameter passed on by name is an lvalue;
std::forward<T>(x)restores the category the caller passed. - Perfect forwarding passes every argument on unchanged, so a wrapper reaches the same constructor as a direct call.
const T&&,std::vector<T>&&andT&&in a class template’s member function are ordinary rvalue references.
Terms in this lesson
- perfect forwarding
- Passing each argument of a wrapper on with the value category the caller gave it, through a forwarding reference and std::forward<T>, so that the wrapped call copies lvalues and moves rvalues exactly as a direct call would.
- reference collapsing
- The rule that turns a reference to a reference, formed through a template parameter or an alias, into one reference: an lvalue reference wins whenever one is involved, and only && combined with && stays &&. With T = std::string&, T&& is std::string&.
References
- Working Draft, Programming Languages: C++ (N4950): [temp.deduct.call], [dcl.ref], [dcl.type.auto.deduct], [forward], [sequence.reqmts], [vector.bool], [func.invoke] (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: F.19, for "forward" parameters, pass by TP&& and only std::forward the parameter (Standard C++ Foundation)
- C++ Core Guidelines: F.18, for "will-move-from" parameters, pass by X&& and std::move the parameter (Standard C++ Foundation)
- C++ Core Guidelines: ES.56, write std::move() only when you need to explicitly move an object to another scope (Standard C++ Foundation)
- P0428R2: Familiar template syntax for generic lambdas (ISO/IEC JTC1/SC22/WG21)
- clang-tidy: bugprone-move-forwarding-reference (LLVM Project)
- clang-tidy: bugprone-use-after-move (LLVM Project)
- clang-tidy: bugprone-forwarding-reference-overload (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress