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)

  1. If expr is a reference, the reference part is ignored.
  2. const / volatile on the argument are kept and become part of T.
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: T is deduced as an lvalue reference (T = Type&), then reference collapsing makes param a Type&.
  • rvalue: normal rules — T is a non-reference, param is Type&&.

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.

  1. A plain name (id-expression) — the declared type as-is.
  2. A more complex lvalue expression — Type&.
  3. 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.