Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Type Deduction Mechanisms

The table below summarizes C++ type deduction features and their respective introductions into the language standard:

MechanismKeyword(s)DescriptionIntroduced
Function template deductiontemplate parametersDeduces template types from function argumentsC++98
Auto type deductionautoDeduces type from initializerC++11
Exact expression typedecltype, decltype(auto)Queries the exact type of an expression (w/o evaluating)C++11/14
Return type deductionauto, decltype(auto)Deduces function return typeC++14
Lambda parameter deductionauto in lambdaDeduces parameter types in generic lambdasC++14
Structured bindingsauto with [ ]Unpacks structured types like tuplesC++17
Class template arg deductionCTADDeduces template types from constructor argsC++17
Non-type template deductionautoDeduce type of constant template parameterC++17
Abbreviated function templatesauto in function paramTemplate parameter deduction in normal function syntaxC++20
Constrained deductionConcepts + autoAdds semantic constraints to type deductionC++20
Compile-time enforcementconsteval, constinitRestricts deduction to compile-time contextC++20

Examples

Auto Type Deduction

int i = 42;
auto x = i;   // x is deduced as int

The auto keyword causes the compiler to deduce x as int, based on the initializer.

Decltype Type Query

int i = 42;
decltype(i) y = i;   // y is also int
auto z = (i);         // auto is int, decltype((i)) is int&

decltype determines the type of an expression without evaluating it. Parentheses can influence whether a value or reference type is deduced.

Function Template Deduction

template<typename T>
void print(T value) {
    std::cout << value << std::endl;
}

print(10);   // T is deduced as int

Template arguments are deduced from the function call's parameter types.

Return Type Deduction

auto add(int a, int b) {
    return a + b;   // return type deduced as int
}

The compiler infers the return type from the return expression when auto is used.

Structured Bindings

std::tuple<int, double> t{1, 2.0};
auto [a, b] = t;  // a is int, b is double

Structured bindings destructure compound types into named variables with deduced types.

Lambda Parameter Deduction

auto lambda = [](auto a, auto b) {
    return a + b;
};

Generic lambdas deduce parameter types during invocation, functioning similarly to templated callables.

Class Template Argument Deduction (CTAD)

template<typename T>
struct Wrapper {
    T value;
    Wrapper(T v) : value(v) {}
};

Wrapper w(123);  // T deduced as int

Constructor arguments guide the deduction of template parameters, eliminating the need for explicit specification.

Non-Type Template Parameter (NTTP) Deduction

 #include <iostream>

template<auto N>
void f() {
    std::cout << N << std::endl;
}

int main() {
    f<5>();     // OK: N is deduced as int
    f<'c'>();   // OK: N is deduced as char
    f<5.0>();   // ❌ Error: double is not a valid non-type template parameter
}

Starting with C++17, non-type template parameters can use auto to infer both the value and the type. In C++20, non-type template parameters (NTTPs) were enhanced to allow a broader set of types, but floating-point types (float, double, long double) are still not allowed as non-type template parameters.

For example, the following class Color is a literal class type with structural semantics, and can be used as NTTP:

struct Color {
    int r, g, b;
    constexpr bool operator==(const Color&) const = default;
};

template<Color C>
struct Widget {
    void print() {
        std::cout << C.r << ", " << C.g << ", " << C.b << "\n";
    }
};

int main() {
    Widget<Color{255, 255, 0}> w; // OK in C++20!
    w.print();
}

But the following can not:

struct NonStructural {
    double d;  // ❌ double is not allowed in structural types, due to comparison and representation issues.
    constexpr bool operator==(const NonStructural&) const = default;
};

template<NonStructural N>
struct T {};  // ❌ Error

Abbreviated Function Templates

void log(auto x) {
    std::cout << x;
}

Function templates can be expressed using auto in parameter declarations, reducing boilerplate syntax.

Concepts and Constrained Deduction

template<typename T>
concept Printable = requires(T t) { std::cout << t; };

void log(Printable auto x) {
    std::cout << x;
}

Concepts restrict template parameters to types satisfying specified requirements. The example ensures that x is printable to an output stream.

consteval and constinit Impact

consteval int square(int x) { return x * x; }

The consteval specifier enforces that the function is evaluated at compile time. This feature is used to guarantee constexpr behavior.

consteval does not itself cause type deduction, but it may participate in deduction contexts. For example, if the return value of a consteval function is used to initialize a variable declared with auto, then type deduction will occur based on the result:

auto y = square(4);  // y deduced as int, square(4) evaluated at compile time

So here, deduction still happens, just as with any function returning a known type. The twist is: the result must be known at compile time.

On the other hand, constinit ensures that a variable with static storage duration (like globals, static members, etc.) is initialized at compile time. It does not mean the variable is constant (unlike const). It ensures that no dynamic initialization will occur — useful for avoiding the static initialization order fiasco.

Similar to consteval, constinit does not perform type deduction itself. But it can interact with deduction:

constinit auto z = square(5);  // auto deduces int

Again, auto deduces the type from the value returned by a consteval function, which satisfies constinit's compile-time requirement.

FeaturePurposeRole in Type Deduction
constevalRequires function to be CT evaluatedMay influence deduction (via result value)
constinitEnsures static init is CTWorks alongside deduction, doesn't perform it
autoDeduce type from initializerCan use values from consteval or constinit

The following example illustrates consteval and constinit putting together:

consteval int factorial(int n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
}

constinit auto fact5 = factorial(5);  // fact5 is int, initialized at compile time

Pitfall: Object Slicing

Base* d = new Derived();
auto b = *d;  // b is Base, object slicing occurs
b.f();        // Calls Base::f(), not Derived::f()

When deducing by value from a base pointer, object slicing occurs, stripping derived-type behavior.

To preserve polymorphic behavior:

auto& b = *d; // b is Base&
b.f();        // Calls Derived::f()