The compiler constantly deduces types for you. Understanding the rules is the foundation of everything modern C++ does.
You write auto x = 42; and the compiler figures out that x is an int. You write a template function and the compiler figures out the template parameter from the argument you pass. Simple, right?
Mostly. But then you hit a case where auto deduces something you didn't expect. A const gets dropped. A reference disappears. An std::initializer_list materializes from nowhere. And suddenly your code doesn't compile, or worse, it compiles but does the wrong thing.
C++11 introduced auto and expanded the role of decltype. C++14 added decltype(auto) and generic lambdas. Type deduction is everywhere now. Understanding these rules isn't optional — it's the price of admission to modern C++.
Three deduction contexts. Hover over each to see examples.
Every template deduction scenario has the same shape. You have a function template and a call to it:
c++ template<typename T> void f(ParamType param); f(expr); // deduce T and ParamType from expr
The compiler uses expr to deduce two things: T itself, and ParamType (which often contains T plus decorators like &, const, *). The deduced types depend on the form of ParamType. Meyers identifies three cases:
When ParamType is a reference or pointer (but not a universal reference), the rules are straightforward:
Step 1: If the argument's type is a reference, ignore the reference part.
Step 2: Then pattern-match the argument's type against ParamType to determine T.
c++ template<typename T> void f(T& param); // ParamType is T& int x = 27; const int cx = x; const int& rx = x; f(x); // T = int, param: int& f(cx); // T = const int, param: const int& f(rx); // T = const int, param: const int&
Notice that when we pass rx (a const int&), the reference part is stripped first, leaving const int. Then T becomes const int. The const is preserved — callers who pass const objects can trust the function won't modify them.
const int to const T& deduces T = int, and param's type is const int&. The end result is the same, but T itself is "smaller."Click each expression to see what T and ParamType are deduced to be.
template<typename T> void f(T& param); and const int cx = 10;, what is T when calling f(cx)?When ParamType has no reference or pointer qualifier, we are passing by value. That means param is a brand-new copy of whatever was passed in. Because it's a copy, it's independent of the original — modifying param won't affect the caller's object.
c++ template<typename T> void f(T param); // pass by value int x = 27; const int cx = x; const int& rx = x; f(x); // T = int, param: int f(cx); // T = int, param: int (const stripped!) f(rx); // T = int, param: int (ref + const stripped!)
Both const and reference are stripped. This makes sense: param is a copy. The fact that the original was const says nothing about whether the copy should be immutable.
const char* const (a const pointer to const char), the pointee's const survives — only the pointer's const is stripped. T becomes const char*.Watch how qualifiers are removed when passing by value. The red crossed-out parts are stripped.
const int to a by-value template parameter, why is const stripped?auto type deduction is almost identical to template type deduction. When you write auto x = expr;, the compiler behaves as if there were a template with auto playing the role of T:
auto auto x = 27; const auto cx = x; const auto& rx = x; auto&& uref = x;
template equivalent // T x = 27 (by value) // const T cx = x (by value) // const T& rx = x (Case 1) // T&& uref = x (Case 2: universal)
One exception: braced initializers. With auto x = {27};, the compiler deduces std::initializer_list<int>. Template deduction would reject the same braced initializer. This is the one place where auto and template deduction diverge.
auto x = {27}; gives you an std::initializer_list<int>, not an int. auto x{27}; in C++11 also gives you an initializer_list (though C++14 changed this to just int for single-element braces). This is the #1 surprise with auto.c++ auto x1 = 27; // int auto x2(27); // int auto x3 = {27}; // std::initializer_list<int> <-- surprise! auto x4{27}; // std::initializer_list<int> in C++11, int in C++14
auto x = {1, 2, 3}; deduce?decltype is the third type deduction mechanism, and it plays by its own rules. Given a name, decltype returns exactly that name's declared type — no stripping, no surprises:
c++ const int i = 0; // decltype(i) is const int bool f(int); // decltype(f) is bool(int) struct Point { int x, y; }; Point p; // decltype(p.x) is int int x = 0; // decltype(x) is int (name → declared type) // decltype((x)) is int& (expression → lvalue ref!)
The last example is the critical gotcha. When decltype is applied to a name, you get the declared type. When applied to an expression (even just wrapping the name in parentheses), you get the expression's category: lvalue expressions become T&, xvalues become T&&, prvalues become T.
decltype(auto) (C++14) lets you use decltype rules with auto syntax. It is especially useful for function return types where you want to preserve the exact type, including references: decltype(auto) f() { return container[i]; } returns a reference if operator[] returns one.Click each expression to see what decltype produces.
int x = 0;, what is the difference between decltype(x) and decltype((x))?Understanding the rules is one thing. Actually seeing what the compiler deduced is another. Meyers offers three approaches, each useful in different situations.
Hover over a variable in your IDE and it shows the type. Fast and convenient for simple cases, but sometimes misleading for complex template types.
The trick: declare a template but never define it, then try to instantiate it. The error message reveals the type:
c++ template<typename T> class TD; // "Type Displayer" — declared, never defined auto x = someExpr; TD<decltype(x)> xType; // error: TD<...> is incomplete // The "..." in the error message IS the deduced type!
typeid(x).name() gives a string, but it's often mangled and strips references and const (it uses the by-value deduction rules). Boost.TypeIndex gives you the real type:
c++ #include <boost/type_index.hpp> using boost::typeindex::type_id_with_cvr; std::cout << type_id_with_cvr<decltype(x)>().pretty_name();
Now let's put everything together. The interactive deduction machine below shows you the deduction rules in action. Choose a function signature and an argument type, and it walks you through the rule application step by step.
Select a function signature and argument, then see the deduction process animated step-by-step.
Function signature:
Argument:
| Case | ParamType | Rule |
|---|---|---|
| 1 | T& or T* | Strip reference from arg, pattern-match. const preserved. |
| 2 | T&& (universal) | Lvalue arg → T = lvalue ref. Rvalue arg → normal rules. |
| 3 | T (by value) | Strip reference and top-level const. Making a copy. |
Type deduction is the foundation on which everything else in Meyers' book is built. Understanding Cases 1-3, the auto/braces exception, and the decltype name-vs-expression distinction equips you for the rest of the journey.
| Item | Key Takeaway |
|---|---|
| Item 1 | Template deduction has three cases based on ParamType's form. |
| Item 2 | auto deduction = template deduction, except braced initializers. |
| Item 3 | decltype returns the declared type for names, category-based type for expressions. |
| Item 4 | Use compiler errors, IDE tooltips, or Boost.TypeIndex to see deduced types. |
"The type system is the immune system of your program. Understanding it isn't pedantry — it's self-defense." — Scott Meyers