C++ Template Argument Deduction | How the Compiler Infers `T`

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:

  1. Inspect each argument’s type.
  2. Match template parameter T to each argument.
  3. 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& with const objects → T deduced as const U if you take T&; design for const 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

TopicNotes
Function templatesDeduce T from arguments; specify explicitly if ambiguous
References / constValue: decay; lvalue ref: preserve const in T
ArraysValue: decay to pointer; ref to array: keep size
CTADC++17; control with deduction guides
DebuggingRead 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.


  • Template basics
  • CTAD
  • Template introduction series

Keywords

C++, template argument deduction, forwarding reference, CTAD, SFINAE.

See also

  • CTAD
  • Deduction guides
  • auto deduction
  • C++20 Concepts
  • constexpr if