C++ Part I · Module 18 · Templates and Generic Programming
Template Argument Deduction
Chapter 18.2 wrote smaller of three and seven and said the compiler works T out from the arguments, which is true and hides the only genuinely intricate part of using templates. The deduction has rules; the rules depend on how the parameter was declared as much as on what was passed; and several of the results are not what a reader would guess. This chapter measures all of them — four parameter forms against five kinds of argument, twenty results, reported by a program that prints what T actually became including its const and reference qualifications. Three of the twenty are worth knowing by heart. A by-value parameter silently discards both const and reference, so passing a const int ampersand deduces plain int and the function operates on a copy whose constness has evaporated. An array passed by value decays to a pointer and loses its length, while the same array passed by reference keeps its type intact — which is the one case where taking a parameter by reference preserves information rather than merely avoiding a copy. And the double-ampersand form deduces, for an lvalue argument, a type that is itself a reference, which is not something you wrote and is the mechanism behind every perfect-forwarding function in the standard library. The chapter closes with the two situations where deduction gives up entirely, and the three different ways to help it.
Advanced18 min readC++Part IFundamentalsTemplates
Part I · Module 18 · Chapter 18.3 · Template Argument Deduction
1. The Question
template <typename T> void f(T param);
const int ci = 2;
f(ci); // what is T?Deduction is the compiler matching the argument's type against the parameter's declared form to work out T. The answer depends on both, which is why there is a table rather than a rule.
The four forms that matter:
template <typename T> void byValue (T);
template <typename T> void byConstRef(const T&);
template <typename T> void byRef (T&);
template <typename T> void byFwd (T&&);2. The Table
Measured by a program that prints T with its qualifications intact:
| argument | byValue(T) | byConstRef(const T&) | byRef(T&) | byFwd(T&&) |
|---|---|---|---|---|
int i | int | int | int | int& |
const int ci | int | int | const int | const int& |
const int& cri | int | int | const int | const int& |
42 (rvalue) | int | int | — | int |
const char[6] | const char* | ARRAY | const ARRAY | — |
The bold cells are the ones worth memorising, and §3 to §5 are each of them.
3. By Value Discards const and Reference
B. a const int:
byValue(T) T = int
C. a const int&:
byValue(T) T = intBoth the const and the reference are gone. A const int& argument deduced T = int, so inside the function param is a plain, modifiable int.
That is correct and occasionally surprising. The parameter is a copy, and the copy's constness is the function's business rather than the caller's — const on the original says you may not change my object, and a copy is not the caller's object.
Two consequences.
You can modify a by-value parameter even when the argument was const, which is harmless and looks wrong at a glance.
And T never carries a reference in this form, so a template taking T by value and returning const T& is the dangling bug Chapter 15.3 §7 measured — there is no caller's object for the reference to refer to.
(The same rules govern auto for variables, which Chapter 3.10 §4 established: auto x = ci; gives a plain int for exactly this reason.)
4. Arrays Decay Through a Value Parameter, Not Through a Reference
E. a const char[6] array:
byValue(T) T = const char*
byConstRef(const T&) T = ARRAY
byRef(T&) T = const ARRAYBy value, the array became a pointer. Chapter 9.2 §2 established array-to-pointer decay as a conversion, and a by-value template parameter triggers it: T is const char* and the length is lost permanently.
By reference, T is the array type itself — length included. That makes the reference form the one way to write a template that knows how long its array argument is:
template <typename T, std::size_t N>
constexpr std::size_t length(const T (&)[N]) { return N; } // N is deducedMeasured:
A. the reference form deduces N:
length(int[7]) = 7
length(double[3]) = 3
length(char[42]) = 42
B. and it is a compile-time constant:
int mirror[length(a)] -> 7 elements
C. the by-value form cannot know:
lengthBroken(a) = 0 (the array decayed to a pointer)N came through for all three arrays, and because it is a template argument it is a compile-time constant — usable as an array bound, which B demonstrates. The by-value version has nothing to report: by the time it is called, the length is gone.
This is the one place in the language where taking a parameter by reference preserves information rather than merely avoiding a copy, and Chapter 9.2 §7 named it as one of the fixes for decay without showing the deduction that makes it work. It is also the mechanism behind std::size for built-in arrays, which Module 19 uses throughout.
5. T&& Deduces a Reference for an Lvalue
The third surprise, and the one with the widest consequences:
A. a plain int: byFwd(T&&) T = int&
B. a const int: byFwd(T&&) T = const int&
D. an rvalue (42): byFwd(T&&) T = intFor an lvalue argument, T is deduced as a reference type — int&, which is not a type anyone wrote. For an rvalue, T is the plain type.
That combination is deliberate. T&& in a template is not an rvalue reference in the Chapter 13.7 sense; it is a forwarding reference, and the deduction is arranged so that the parameter ends up matching whatever was passed:
| argument | T deduced | T&& becomes |
|---|---|---|
lvalue int | int& | int& && → int& |
const int lvalue | const int& | const int& |
rvalue int | int | int&& |
The int& && → int& step is reference collapsing: a reference to a reference is just a reference, and the result is an lvalue reference unless both are rvalue references.
So a T&& parameter binds to anything and remembers which it was, which is exactly what a function needs if it is going to pass the argument on without losing its category. Chapter 13.9 §4 listed passing on such a parameter as one of the three places std::move belongs; std::forward is the version that preserves the category rather than always claiming rvalue, and Chapter 18.9 §6 uses it in the make_unique shape.
One caution: T&& is a forwarding reference only when T is being deduced. void g(std::string&&) is an ordinary rvalue reference, and so is Vec<T>&& inside a class template where T is already fixed.
6. What Is Not Deduced
Deduction looks only at the argument types. It does not consider:
The return type. template <typename T> T make(); cannot deduce T from auto x = make(); — measured, the explicit form make<int>() compiles and the bare call cannot.
Default template arguments — they supply a value, they do not participate in matching.
And conversions. Deduction is a pattern match, not an overload resolution: T is found by making the parameter form fit the argument type exactly, with only a small fixed set of adjustments (the decay in §4, dropping top-level const in §3). It will not deduce T = double from an int argument, which is §7.
7. Failure One: Conflicting Deductions
template <typename T> const T& smaller(const T& a, const T& b);
smaller(1, 2.0);error: no matching function for call to 'smaller'
note: candidate template ignored: deduced conflicting types for parameter 'T'
('int' vs. 'double')One T, two arguments, two different types. The first argument says T = int, the second says T = double, and deduction has no mechanism for choosing or converting — the note names both candidates for T explicitly, which is a genuinely helpful diagnostic.
Note what did not happen: no implicit conversion of 1 to 1.0. In an ordinary function call that conversion would be applied; in deduction it is not, because deduction runs before there is a function to convert arguments for.
8. Failure Two: Braced Initialisers
smaller({1,2}, {3,4});error: no matching function for call to 'smaller'A braced initialiser list has no type, so there is nothing to deduce from. {1,2} could be an int[2], a std::vector<int>, a std::pair, or an aggregate — the meaning depends on the parameter type, which is what deduction is trying to find.
The exception worth knowing: a parameter declared std::initializer_list<T> can deduce T from a braced list, which is how std::vector<int> v{1,2,3} works. Outside that special case, braces and deduction do not mix.
9. The Three Fixes
explicit argument : smaller<double>(1, 2.0) = 1.0
two parameters : smaller2(1, 2.0) = 1.0
cast at the call : smaller(1.0, 2.0) = 1.0Supply T explicitly. smaller<double>(1, 2.0) — deduction is skipped entirely, and the int argument converts to double normally. This is the smallest fix and usually the right one.
Use two template parameters. template <typename A, typename B> auto smaller2(const A&, const B&) — each deduces independently. This changes the requirement to a cross-type comparison and changes the return type:
decltype(smaller2(1, 2.0)) is double? 1 is a reference? 0auto deduced double, by value — necessarily, because the common type of an int and a double is a double and there is no double object among the arguments to return a reference to. Chapter 18.2 §7 noted this; here is why it is forced.
Or fix the call. smaller(1.0, 2.0) — both arguments the same type, nothing to reconcile. Often the honest answer, because a call mixing types may be a sign the caller was unclear.
10. When to Supply It Explicitly
Four situations, and the first two are the common ones.
When the type cannot be deduced — §6's return-type case: make<Transaction>().
When deduction would conflict — §7.
When you want a different type than the arguments imply — smaller<long>(a, b) to compare as long.
And when you want to defeat a deduction you do not like — Chapter 18.2 §9 measured smaller<const char*> forcing the generic path and comparing pointers instead of text. That was an illustration of a mistake; the same mechanism is occasionally what you want.
(Class templates deduce too, since C++17 — Box b{42} without the angle brackets. That is class template argument deduction, it follows different rules from everything above, and it is Chapter 18.4 §6's subject.)
11. What to Actually Do
Expect by-value parameters to drop const and references — and never return const T& from one.
Use const T (&)[N] when a template needs a built-in array's length.
Read T&& as "binds to anything" when T is deduced — and as a plain rvalue reference when it is not.
Supply T explicitly for a return-type-only parameter, a conflict, or a deliberate different type.
Prefer two template parameters when the arguments genuinely differ in type, and return auto.
Do not pass braced initialisers to a deduced parameter unless it is an initializer_list.
And remember deduction precedes conversion — it explains both failures.
12. Mistakes Worth Making Once
Expecting a by-value T to keep const. Measured: const int& deduced T = int.
Returning const T& from a by-value parameter. Chapter 15.3 §7's dangling reference.
Expecting an array to keep its length through a by-value parameter. Measured: T = const char*.
Reading T&& as an rvalue reference when T is deduced. Measured: an lvalue gave T = int&.
Assuming T&& is a forwarding reference inside a class template where T is already fixed. It is not.
Expecting smaller(1, 2.0) to promote the int. Measured: deduced conflicting types for parameter 'T'.
Passing {1,2} to a deduced parameter. A braced list has no type.
Trying to deduce from the return type. Measured: only the explicit form compiles.
Returning const A& from a two-parameter template. The common type may be neither — measured double, by value.
13. Interview Relevance
"What does T deduce to for const int& passed by value?" Plain int — both the const and the reference are dropped, because the parameter is a copy.
"And through T&?" const int — the const is preserved because the parameter refers to the caller's object.
"What about T&&?" int& for an lvalue, int for an rvalue — a forwarding reference, with reference collapsing turning int& && into int&. Getting this right is the discriminator.
"When is T&& not a forwarding reference?" When T is not being deduced — a fixed type, or a class template's parameter.
"What happens to an array?" Decays to a pointer by value; keeps its type and length by reference, which is how f(const T (&)[N]) works.
"Why does smaller(1, 2.0) fail?" Conflicting deductions for one T. Deduction precedes conversion, so no promotion happens.
"How do you fix it?" Explicit argument, two parameters, or matching arguments.
14. What You Should Be Able to Say Now
- Deduction matches the argument's type against the parameter's declared form, so the answer depends on both.
- Measured, by value: a
const intand aconst int&both deducedT = int—constand reference dropped. - Because the parameter is a copy, and the caller's
constwas about the caller's object. - So never return
const T&from a by-value parameter — Chapter 15.3 §7. - Measured, arrays: by value
T = const char*(decayed); byconst T&,Tis the array type, length included. const T (&)[N]is the only way to know a built-in array's length in a function.- Measured,
T&&: an lvalueintgaveT = int&; aconst intgaveconst int&; an rvalue gaveint. - That is a forwarding reference, and reference collapsing turns
int& &&intoint&. - It binds to anything and remembers which — the mechanism behind
make_uniqueandemplace_back. T&&is only a forwarding reference whenTis deduced.- Deduction ignores the return type, default template arguments, and conversions.
- Measured failure:
smaller(1, 2.0)gavededuced conflicting types for parameter 'T' ('int' vs. 'double'). - Measured failure: a braced initialiser has no type and cannot be deduced from — except for
std::initializer_list<T>. - Three fixes: explicit argument, two template parameters, or matching arguments.
- Measured: the two-parameter form's
autoreturn deduceddouble, by value — there was no object of the common type. - Deduction precedes conversion, which explains both failures at once.
15. Exercises
Exercise 1 — fill in the table. For std::string s, const std::string cs, and std::string("tmp"), give T for all four parameter forms.
Exercise 2 — the copy argument. Explain why a by-value parameter drops const, and name the variable-declaration rule that behaves the same way.
Exercise 3 — array length. Write a template that returns the number of elements in a built-in array, and say which parameter form makes it possible.
Exercise 4 — forwarding. For template <typename T> void g(T&&), give T and the parameter type for an lvalue int, a const int lvalue, and 42.
Exercise 5 — collapsing. Work int& && and int&& && down to single references and state the rule.
Exercise 6 — not a forwarding reference. Give two declarations where T&& is an ordinary rvalue reference.
Exercise 7 — predict the failure. Say what happens for smaller(1, 2.0) and smaller({1},{2}), with the diagnostics.
Exercise 8 — the three fixes. Apply each to smaller(1, 2.0) and say what each changes about the requirement and the return type.
Exercise 9 — the ordering. Explain how "deduction precedes conversion" accounts for both failures.
16. Summary
- Deduction depends on the parameter's form as much as the argument's type — hence a table.
- Measured: by value,
const intandconst int&both gaveT = int— the parameter is a copy. - Measured: an array gave
const char*by value and kept its type and length by reference — the one case where a reference preserves information. - Measured:
T&&gaveint&for an lvalue andintfor an rvalue — a forwarding reference, via reference collapsing. - It is only a forwarding reference when
Tis deduced. - Deduction ignores return types, defaults and conversions.
- Measured failures: conflicting deductions (
'int' vs. 'double') and a braced initialiser with no type. - Three fixes: explicit argument, two parameters with an
autoreturn (measureddouble, by value), or matching arguments. - Deduction precedes conversion — the single idea that explains both failures.
Function templates are complete, and the other half of the mechanism is the one the next two modules are built on. Every container in the standard library is a class parameterised by a type, and the shape has properties a function template does not: each instantiation is a genuinely distinct type with its own members and its own statics, the member functions are instantiated only if they are used, and since C++17 the type argument can often be deduced from a constructor call just as a function's can. The next chapter is that shape — written out, measured, and with the one question people get wrong about it settled: whether a Box<int> and a Box<double> have anything to do with each other, which it turns out they do not, in a way the compiler will state plainly if you ask it to assign one to the other.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
