C++ Template Argument Deduction | How the Compiler Infers `T`
이 글의 핵심
How template arguments are inferred from call sites: decay, T&, T&&, arrays, and C++17 class template argument deduction.
What is template argument deduction?
When you call a function template without explicit template arguments, the compiler deduces template parameters from the types of the arguments. That process is template argument deduction. APIs like std::make_pair and std::make_unique rely on it.
Basic example
template<typename T>
T add(T a, T b) {
return a + b;
}
int main() {
add(1, 2); // T = int
add(1.0, 2.0); // T = double
// add(1, 2.0); // error: T cannot be both int and double
add<double>(1, 2.0); // explicit
return 0;
}
Deduction steps:
- Inspect each argument’s type.
- Match template parameter
Tto each argument. - If all arguments agree on
T, success; otherwise a hard error.
Why does add(1, 2.0) fail? The first argument wants T = int, the second T = double—the deductions conflict.
Deduction rules in detail
Rule 1: Pass by value ⇒ decay
For by-value parameters, top-level const, volatile, and reference qualifiers are stripped; arrays and functions decay to pointers.
template<typename T>
void by_value(T x) { (void)x; }
int i = 0;
const int ci = 10;
int& ref = i;
int arr[5];
by_value(i); // T = int
by_value(ci); // T = int (const dropped)
by_value(ref); // T = int (reference stripped)
by_value(arr); // T = int* (array decay)
Practice: Value parameters copy the argument, so cv-qualifiers on the argument do not affect T. Array size is lost after decay to pointer.
Rule 2: Pass by reference ⇒ type preserved
For reference parameters, const and reference are preserved in the deduction of T.
template<typename T>
void by_ref(T& x) { (void)x; }
int i = 0;
const int ci = 10;
by_ref(i); // T = int
by_ref(ci); // T = const int
// by_ref(5); // error: cannot bind non-const lvalue ref to temporary
Tips:
- Need to mutate:
T& - Read-only:
const T&(also binds to temporaries) - Perfect forwarding:
T&&(forwarding reference)
Rule 3: Pointers
Pointers distinguish pointer-to-const vs const pointer.
template<typename T>
void by_ptr(T* p) { (void)p; }
int x = 10;
const int cx = 20;
by_ptr(&x); // T = int
by_ptr(&cx); // T = const int
Practical examples
Example 1: Mismatched argument types
template<typename T>
T add(T a, T b) { return a + b; }
// add(1, 2.0); // error
Fix 1 — explicit template argument
add<double>(1, 2.0);
Fix 2 — two template parameters
template<typename T1, typename T2>
auto add(T1 a, T2 b) {
return a + b;
}
Fix 3 — std::common_type
template<typename T1, typename T2>
std::common_type_t<T1, T2> add(T1 a, T2 b) {
return a + b;
}
Recommendation: Two template parameters plus auto return type is often the most flexible.
Example 2: Perfect forwarding
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int x = 10;
wrapper(x); // T = int&
wrapper(10); // T = int
wrapper(std::move(x)); // T = int
For T&& in a deduced context, lvalues yield T = U& (reference collapsing); rvalues yield T = U.
Example — make_unique style
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
auto ptr = make_unique<Widget>(10, "hello");
References, const, and deduction
By value: T drops references and top-level cv. By T&: T can be const U when you pass const objects.
Tip: For generic “read-only” APIs, prefer const T& to avoid unnecessary copies when you only need T by value semantics.
Arrays and decay
Arrays passed by value decay; pass by reference to preserve extent:
template<typename T, size_t N>
void g(T (&a)[N]) {
std::cout << "Array size: " << N << '\n';
}
int arr[3] = {1, 2, 3};
g(arr); // T = int, N = 3
Class templates: CTAD (C++17)
Constructor arguments can deduce class template parameters, e.g. std::pair, std::vector, std::optional.
std::pair p(1, 2.0);
std::vector v = {1, 2, 3};
std::optional opt(42);
Tip: User-defined class templates can use deduction guides when constructor inference is ambiguous.
template<typename T>
Container(T) -> Container<T>;
Common issues
- Conflicting parameter types → explicit arguments or multiple template parameters.
T&withconstobjects →Tdeduced asconst Uif you takeT&; design forconst T&when appropriate.- Many constructors → wrong type deduced; constrain with guides or factory functions.
Debugging deduction failures
Read the instantiation note to see which T was chosen, then check whether that T satisfies the template body. Add static_assert or C++20 concepts for clearer errors.
Summary
| Topic | Notes |
|---|---|
| Function templates | Deduce T from arguments; specify explicitly if ambiguous |
References / const | Value: decay; lvalue ref: preserve const in T |
| Arrays | Value: decay to pointer; ref to array: keep size |
| CTAD | C++17; control with deduction guides |
| Debugging | Read instantiation stack; add constraints |
FAQ
Q1: When does deduction apply?
A: Whenever you omit explicit template arguments on a function template call.
Q2: It failed—now what?
A: Specify func<int>(...), adjust argument types, or add another template parameter.
Q3: Pitfalls with references?
A: const objects can make T const-qualified if you use T& and try to mutate.
Q4: CTAD?
A: Use C++17 CTAD for concise vector/pair-style initialization; add guides when needed.
Q5: What is a forwarding reference?
A: T&& with T deduced from an argument—used with std::forward for perfect forwarding.
In one sentence: Deduction lets you omit types at call sites; knowing decay, references, arrays, and CTAD avoids common surprises.
Related posts
- Template basics
- CTAD
- Template introduction series
Keywords
C++, template argument deduction, forwarding reference, CTAD, SFINAE.
See also
- CTAD
- Deduction guides
autodeduction- C++20 Concepts
constexpr if