Without a clear model of type deduction, effective modern C++ is hard: function templates, auto, decltype, decltype(auto) — and even more contexts in C++17–C++23. The fundamental rules were not cancelled; later standards extended them (CTAD, abbreviated function templates, concepts, auto NTTPs, deducing this).
Everything rests on three pillars.
1. Function templates
template <typename T>
void f(ParamType param);
f(expr); // the compiler deduces T and ParamType separately
T and ParamType are different. If ParamType is const T& and the argument is int, then T = int and ParamType = const int&.
Case A: ParamType is a reference or pointer (not universal)
- If
expris a reference, the reference part is ignored. const/volatileon the argument are kept and become part ofT.
template <typename T> void f_ref(T& param);
template <typename T> void f_cref(const T& param);
int x = 27;
const int cx = x;
const int& rx = x;
// f_ref(T& param):
f_ref(x); // T = int, param = int&
f_ref(cx); // T = const int, param = const int&
f_ref(rx); // T = const int, param = const int&
// f_ref(27); // error: non-const lvalue reference cannot bind to an rvalue
// f_cref(const T& param):
f_cref(x); // T = int, param = const int&
f_cref(cx); // T = int, param = const int& (const already in ParamType)
f_cref(rx); // T = int, param = const int&
f_cref(27); // T = int, param = const int& (const& can bind to an rvalue)
Even though the template is declared as T& without an explicit const, passing cx is valid: the compiler deduces T as const int.
Case B: universal (forwarding) reference T&&
It is universal only when T is being deduced in that position (T&&, not std::vector<T>&& or const T&&).
- lvalue:
Tis deduced as an lvalue reference (T = Type&), then reference collapsing makesparamaType&. - rvalue: normal rules —
Tis a non-reference,paramisType&&.
Reference collapsing: &+& → &; &+&& → &; &&+& → &; &&+&& → &&.
template <typename T> void f_fwd(T&& param);
int x = 27;
const int cx = x;
f_fwd(x); // lvalue → T = int&, param = int&
f_fwd(cx); // lvalue → T = const int&, param = const int&
f_fwd(27); // rvalue → T = int, param = int&&
This is the only template case where T itself may be deduced as a reference.
Case C: pass by value (T)
A copy: references are stripped; top-level const / volatile are stripped too (the copy can be mutated independently).
template <typename T> void f_val(T param);
int x = 27;
const int cx = x;
const int& rx = x;
const char* const ptr = "Hello";
f_val(x); // T = int
f_val(cx); // T = int (const stripped)
f_val(rx); // T = int (reference and const stripped)
f_val(ptr); // T = const char*
// top-level const on the pointer is gone; const on the data remains
Arrays and functions
By value (T) — decay to a pointer. By reference (T&) — the full type is preserved.
const char name[] = "C++"; // const char[4]
template <typename T> void f1(T param);
template <typename T> void f2(T& param);
f1(name); // T = const char*, param = const char*
f2(name); // T = const char[4], param = const char(&)[4]
Hence helpers like:
template <typename T, std::size_t N>
constexpr std::size_t array_size(T (&)[N]) noexcept {
return N;
}
A function passed by reference becomes a function reference (void(&)()), not a pointer.
2. auto
auto matches template deduction: auto is like T; modifiers (const auto&, auto&&) are like ParamType.
int x = 10;
const int cx = x;
const int& rx = x;
auto a = x; // int (copy)
auto b = cx; // int (const stripped)
auto c = rx; // int (reference and const stripped)
const auto& d = x; // const int&
auto& e = cx; // const int& (const became part of the type)
auto&& f1 = x; // lvalue → int&
auto&& f2 = 10; // rvalue → int&&
Exception: braced initializer
auto x1 = 27; // int
auto x2 = {27}; // std::initializer_list<int>
template <typename T> void f(T param);
// f({27}); // error: type is not deduced from a braced initializer list
3. decltype and decltype(auto)
decltype does not “clean” the type: it reports the exact type of the entity / expression.
- A plain name (
id-expression) — the declared type as-is. - A more complex lvalue expression —
Type&. - A more complex rvalue expression —
Type.
int x = 0;
decltype(x) a = x; // int
decltype((x)) b = x; // int& ((x) is an lvalue expression)
decltype(auto) (C++14): deduce with decltype rules, not template rules — keep references and const.
std::string& get_name();
auto name1 = get_name(); // std::string — a copy
decltype(auto) name = get_name(); // std::string&
Watch extra parentheses in return:
decltype(auto) ok() {
int x = 42;
return x; // int
}
decltype(auto) buggy() {
int x = 42;
return (x); // int& to a local — dangling
}
Cheat sheet
| Form | const |
& |
Useful when |
|---|---|---|---|
auto x = expr; |
stripped | stripped | independent copy |
const auto& x = expr; |
always present | always present | read without copying (lvalue and rvalue) |
auto& x = expr; |
preserved | always present | mutate an original lvalue |
auto&& x = expr; |
preserved | collapsed | forwarding / std::forward |
decltype(auto) x = expr; |
preserved | exactly as in the expression | perfect return-type propagation |
Formulations and examples follow the usual T& walkthrough and the three-pillar model (templates / auto / decltype) in the Effective Modern C++ style; C++17–C++23 add more places where deduction appears, they do not replace cases A–C.