Class template argument deduction (CTAD) and deduction guides

Class template argument deduction (CTAD) C++17

Before C++17, when creating objects from class templates, you had to explicitly specify all template arguments:

#include <utility>

int main()
{
    std::pair<double, int> measurement{ 98.6, 37 };  // temperature and reading count

    return 0;
}

Starting in C++17, the compiler can deduce template types from initializers. This feature is called class template argument deduction (CTAD):

#include <utility>

int main()
{
    std::pair measurement{ 98.6, 37 };  // CTAD deduces std::pair<double, int>

    return 0;
}

The compiler examines the initializer types and determines the appropriate template arguments automatically.

CTAD requires no template argument list

CTAD only activates when you omit the template argument list entirely. Partial lists trigger errors:

#include <utility>

int main()
{
    std::pair<> score{ 100, 95 };       // Error: empty list, not "no list"
    std::pair<int> range{ 10, 20 };     // Error: partial list, second type not deduced

    return 0;
}

Either provide all arguments explicitly or provide none and let CTAD handle it.

Literal suffixes affect deduction

Since CTAD examines initializer types, literal suffixes change what gets deduced:

#include <utility>

int main()
{
    std::pair sizes1{ 640, 480 };       // pair<int, int>
    std::pair sizes2{ 640u, 480u };     // pair<unsigned int, unsigned int>
    std::pair sizes3{ 640.0, 480.0 };   // pair<double, double>
    std::pair sizes4{ 640.0f, 480.0f }; // pair<float, float>

    return 0;
}

Deduction guides for custom templates

Standard library templates like std::pair include built-in deduction guides. For your own aggregate templates, C++17 requires explicit guides:

template <typename T, typename U>
struct Result
{
    T value{};
    U errorCode{};
};

// Deduction guide: Result initialized with types T and U becomes Result<T, U>
template <typename T, typename U>
Result(T, U) -> Result<T, U>;

int main()
{
    Result<double, int> r1{ 3.14, 0 };  // Explicit - always works
    Result r2{ 3.14, 0 };               // CTAD - requires deduction guide in C++17

    return 0;
}

The deduction guide tells the compiler how to map initialization patterns to template arguments.

Understanding deduction guide syntax

template <typename T, typename U>
Result(T, U) -> Result<T, U>;

Breaking this down:

  • template <typename T, typename U> declares the template parameters
  • Result(T, U) describes the pattern to match (a Result with two arguments)
  • -> Result<T, U> specifies what type to deduce

When the compiler sees Result r{ 3.14, 0 }, it matches Result(double, int) to the guide and deduces Result<double, int>.

Single-type templates

For templates with one type parameter used multiple times:

template <typename T>
struct Bounds
{
    T lower{};
    T upper{};
};

template <typename T>
Bounds(T, T) -> Bounds<T>;

int main()
{
    Bounds range{ 0, 100 };    // Bounds<int>
    Bounds limits{ 0.0, 1.0 }; // Bounds<double>

    return 0;
}
Note
C++20 automatically generates deduction guides for aggregates, making explicit guides unnecessary for simple cases. The examples above work without guides in C++20 and later.

Standard library types like std::pair include pre-defined guides, which is why they work with CTAD immediately.

Default template arguments

Template parameters can have default values:

template <typename T = int, typename U = int>
struct Entry
{
    T key{};
    U data{};
};

template <typename T, typename U>
Entry(T, U) -> Entry<T, U>;

int main()
{
    Entry<double, std::string> item1{ 1.5, "active" };  // Explicit
    Entry item2{ 2.5, "pending" };                      // CTAD: Entry<double, const char*>
    Entry item3{};                                      // Defaults: Entry<int, int>

    return 0;
}

When neither explicit arguments nor initializers provide type information, defaults apply.

CTAD limitations

Non-static member initialization

CTAD cannot deduce types in member declarations:

#include <utility>

struct SensorLog
{
    std::pair<double, int> reading1{ 98.6, 1 };  // OK - explicit types
    std::pair reading2{ 98.6, 1 };               // Error - CTAD not allowed here
};

int main()
{
    std::pair reading{ 98.6, 1 };  // OK - CTAD works in function scope
    return 0;
}

Always specify template arguments explicitly for class member variables.

Function parameters

CTAD works on template arguments, not parameters. Function parameter types cannot use CTAD:

#include <iostream>
#include <utility>

void logReading(std::pair data)  // Error: can't deduce parameter type
{
    std::cout << data.first << '\n';
}

int main()
{
    std::pair reading{ 98.6, 1 };
    logReading(reading);

    return 0;
}

Use function templates instead:

#include <iostream>
#include <utility>

template <typename T, typename U>
void logReading(std::pair<T, U> data)
{
    std::cout << data.first << '\n';
}

int main()
{
    std::pair reading{ 98.6, 1 };  // CTAD deduces pair<double, int>
    logReading(reading);           // Template deduces T=double, U=int

    return 0;
}

Summary

Class template argument deduction (CTAD) lets the compiler determine template arguments from initializers, reducing verbosity when creating template objects.

CTAD activation: Omit the entire template argument list. Empty <> or partial lists cause errors.

Deduction guides map initialization patterns to template instantiations. C++17 requires them for user-defined aggregates; C++20 generates them automatically.

Default template arguments provide fallback types when neither explicit arguments nor initializers supply type information.

Limitations: CTAD cannot be used with non-static member initializers or function parameter declarations.

CTAD simplifies generic code by eliminating redundant type specifications while maintaining full type safety.