Deduction and forwarding
How types are worked out, and how to pass them on unchanged.
By the end of this chapter you can
- Explain the difference between auto and template deduction
- Write a perfectly forwarding wrapper
- Explain what a forwarding reference is and how to spot one
Chapter 5.1 said the compiler works out T from the arguments. This chapter is
about the rules it uses — because they are not quite what you would guess, and
the surprises account for a good share of template bugs.
Deduction drops references and const
#include <iostream>
#include <string>
#include <type_traits>
template <class T>
void show_deduced(T) {
std::cout << " is reference: " << std::boolalpha << std::is_reference_v<T>
<< ", is const: " << std::is_const_v<std::remove_reference_t<T>> << '\n';
}
int main() {
std::string value = "hello";
const std::string& ref = value;
std::cout << "passing a std::string:\n"; show_deduced(value);
std::cout << "passing a const std::string&:\n"; show_deduced(ref);
}Both deduce T = std::string. A by-value parameter always gets its own copy, so
the reference and the const are irrelevant to it and are stripped.
auto follows the same rules, which is the connection worth remembering:
#include <iostream>
#include <string>
#include <type_traits>
int main() {
const std::string original = "hello";
auto copy = original; // std::string — const dropped
const auto& observer = original; // const std::string& — as written
auto& mutable_ref = const_cast<std::string&>(original);
std::cout << std::boolalpha;
std::cout << "copy is const: " << std::is_const_v<decltype(copy)> << '\n';
std::cout << "observer is const&: "
<< std::is_reference_v<decltype(observer)> << '\n';
copy += " world"; // legal: it is a separate object
std::cout << "copy: " << copy << ", original: " << original << '\n';
(void)mutable_ref;
}This is the Chapter 1.2 pitfall stated precisely: auto x = container.front();
copies even when front() returns a reference, because plain auto deduces by
value and drops the reference. Write auto& or const auto& when you meant to
bind.
decltype, and decltype(auto)
decltype(expr) gives the declared type of an expression, keeping references
and const:
#include <iostream>
#include <string>
#include <type_traits>
#include <vector>
int main() {
std::vector<std::string> words{"alpha"};
auto a = words.front(); // std::string — a copy
decltype(words.front()) b = words.front(); // std::string& — a reference
std::cout << std::boolalpha;
std::cout << "auto: reference? " << std::is_reference_v<decltype(a)> << '\n';
std::cout << "decltype: reference? " << std::is_reference_v<decltype(b)> << '\n';
b += "!";
std::cout << "modifying b changed the container: " << words.front() << '\n';
}decltype(auto) combines them: deduce like decltype — keeping references —
rather than like auto. That matters most for a function that forwards a return
value:
#include <iostream>
#include <type_traits>
#include <vector>
std::vector<int> data{1, 2, 3};
int& element() { return data[0]; }
// auto strips the reference: the caller gets a copy.
auto by_auto() { return element(); }
// decltype(auto) keeps it: the caller gets the reference.
decltype(auto) by_decltype_auto() { return element(); }
int main() {
std::cout << std::boolalpha;
std::cout << "by_auto returns a reference: "
<< std::is_reference_v<decltype(by_auto())> << '\n';
std::cout << "by_decltype_auto returns a reference: "
<< std::is_reference_v<decltype(by_decltype_auto())> << '\n';
by_decltype_auto() = 99;
std::cout << "assigning through it changed data[0]: " << data[0] << '\n';
}Forwarding references
Here is the rule that looks like a special case and is the whole basis of
generic wrappers. In a deduced context, T&& is not an rvalue reference —
it is a forwarding reference, and it binds to anything:
#include <iostream>
#include <string>
#include <type_traits>
template <class T>
void inspect(T&& value) {
std::cout << " T is "
<< (std::is_lvalue_reference_v<T> ? "an lvalue reference" : "not a reference")
<< ", parameter binds to "
<< (std::is_lvalue_reference_v<T> ? "an lvalue" : "an rvalue") << '\n';
(void)value;
}
int main() {
std::string named = "hello";
const std::string constant = "world";
std::cout << "passing an lvalue:\n"; inspect(named);
std::cout << "passing a const lvalue:\n"; inspect(constant);
std::cout << "passing an rvalue:\n"; inspect(std::string{"temp"});
}The mechanism is reference collapsing. When T is deduced as std::string&
(for an lvalue), the parameter T&& becomes std::string& &&, which collapses
to std::string&. When T is deduced as std::string (for an rvalue), it stays
std::string&&. One declaration, both categories.
std::forward
A forwarding reference preserves the category on the way in. Passing it on loses that, because a named parameter is an lvalue — however it was initialised:
#include <iostream>
#include <string>
#include <utility>
void consume(const std::string&) { std::cout << " copy overload\n"; }
void consume(std::string&&) { std::cout << " move overload\n"; }
template <class T>
void naive(T&& value) {
consume(value); // `value` is a name: always an lvalue
}
template <class T>
void forwarding(T&& value) {
consume(std::forward<T>(value)); // restores the original category
}
int main() {
std::string named = "x";
std::cout << "naive, lvalue: "; naive(named);
std::cout << "naive, rvalue: "; naive(std::string{"t"});
std::cout << "forwarding, lvalue: "; forwarding(named);
std::cout << "forwarding, rvalue: "; forwarding(std::string{"t"});
}naive calls the copy overload every time, even when handed a temporary — the
move is silently lost. std::forward<T> casts back to the original category:
an lvalue reference stays an lvalue, an rvalue becomes an rvalue again.
The rule: std::forward<T>(x) on a forwarding reference, std::move(x) on a
concrete rvalue reference. std::forward without the template argument does not
compile, which is a useful guardrail.
A perfectly forwarding wrapper
Put it together and you get the shape every factory and every emplace uses:
#include <iostream>
#include <memory>
#include <string>
#include <utility>
struct Widget {
std::string name;
int size;
Widget(std::string n, int s) : name(std::move(n)), size(s) {
std::cout << " constructed " << name << " (" << size << ")\n";
}
};
// Forwards any number of arguments, preserving each one's value category.
template <class T, class... Args>
std::unique_ptr<T> make(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
int main() {
std::string name = "alpha";
auto a = make<Widget>(name, 1); // name copied
auto b = make<Widget>(std::string{"beta"}, 2); // temporary moved
auto c = make<Widget>(std::move(name), 3); // name moved
std::cout << "name after being moved from: \"" << name << "\"\n";
std::cout << a->name << ' ' << b->name << ' ' << c->name << '\n';
}Args&&... is a pack of forwarding references and
std::forward<Args>(args)... expands to one std::forward per argument.
Chapter 5.6 covers packs; this is std::make_unique, essentially in full.
Constraining a forwarding reference
A forwarding reference binds to everything, which makes it greedy — it will beat your copy constructor for a non-const lvalue. Constrain it:
#include <concepts>
#include <iostream>
#include <string>
#include <utility>
class Name {
public:
// Without the constraint, this beats the copy constructor for a
// non-const Name lvalue, because that needs no qualification conversion.
template <class T>
requires (!std::same_as<std::remove_cvref_t<T>, Name>)
explicit Name(T&& value) : text_(std::forward<T>(value)) {
std::cout << " template constructor\n";
}
Name(const Name& other) : text_(other.text_) { std::cout << " copy constructor\n"; }
const std::string& text() const { return text_; }
private:
std::string text_;
};
int main() {
Name a{std::string{"alpha"}};
Name b{a}; // must use the copy constructor
std::cout << b.text() << '\n';
}std::remove_cvref_t<T> strips references and const so the check sees the
underlying type. Without the constraint, Name b{a} calls the template with
T = Name&, tries to initialise a std::string from a Name, and fails with
an error deep inside the constructor.