From Explicit Template Arguments to CTAD

Before C++17, class template arguments usually had to be specified explicitly:

Stack<int> stack;

Since C++17, the compiler can often deduce these arguments from an initializer:

Stack stack{42}; // Stack<int>

This mechanism is called Class Template Argument Deduction, or CTAD. It applies when a class template name is used without a template argument list and the available deduction candidates provide enough information to determine all required arguments.

Deduction from a Constructor

Consider a stack that stores its elements in a std::vector and can be initialized with one value:

#include <vector>

template<typename T>
class Stack
{
    std::vector<T> elems;

public:
    Stack() = default;

    Stack(const T& elem)
        : elems{elem}
    {
    }

    void push(const T& elem)
    {
        elems.push_back(elem);
    }

    const T& top() const
    {
        return elems.back();
    }
};

The constructor parameter contains enough information to deduce T:

Stack stack{42}; // T = int, type is Stack<int>

The explicitly defaulted constructor deserves attention:

Stack() = default;

Once a class declares another constructor, the compiler no longer implicitly declares the ordinary default constructor. Writing = default requests it explicitly. However, this constructor does not help CTAD because its parameter list contains no information from which T could be deduced:

Stack<int> first; // valid: the specialization is known
Stack second;     // error: T cannot be deduced

Why braces matter

The member initializer uses braces:

Stack(const T& elem)
    : elems{elem}
{
}

For T = int, elems{elem} initializes the vector from an initializer list containing one element. Writing elems(elem) instead could select the vector constructor that interprets the integer as a size:

std::vector<int> oneElement{5}; // one element with the value 5
std::vector<int> fiveElements(5); // five value-initialized elements

How the Standard Models CTAD

The C++ standard describes CTAD through a set of fictional functions called guides. For each constructor of a class template, the compiler forms a candidate with the same parameter list and a return type naming the corresponding class specialization.

For exposition, the constructor

template<typename T>
class Stack
{
public:
    Stack(const T&);
};

can be pictured as producing a candidate similar to this:

// Exposition only: this function is not actually declared.
template<typename T>
auto F(const T&) -> Stack<T>;

For the initializer 42, template argument deduction produces T = int. The return type of the selected candidate is therefore Stack<int>.

The standard states that the class placeholder is replaced by the return type selected by overload resolution for class template deduction. Conceptually, the declaration can be read as follows:

// Written by the programmer:
Stack stack{42};

// Type selected through CTAD:
Stack<int> stack{42};

After the specialization has been determined, the declaration must still initialize an object of that type successfully. A constructor-generated candidate can both determine the specialization and identify the constructor that performs initialization. A user-defined deduction guide, on the other hand, can determine the specialization while a different, real constructor initializes the object.

This model is specified in [dcl.type.class.deduct] and [over.match.class.deduct]. It describes the language semantics, not a required sequence of internal compiler passes.

String Literals: Reference versus Value

String literals expose an important detail of template argument deduction. The literal "hello" has the array type const char[6]: five characters followed by a null terminator.

When the constructor accepts its argument by reference, the array type is preserved:

Stack(const T& elem);

For the following declaration, deduction produces T = char[6]. The const qualification is supplied by the parameter pattern const T&:

Stack stack{"hello"}; // attempts to form Stack<char[6]>

This is not a useful specialization for the implementation above. A raw array does not satisfy the requirements expected of an ordinary std::vector element, so attempting to instantiate std::vector<char[6]> makes the declaration ill-formed.

One possible response is to accept the constructor argument by value:

#include <utility>
#include <vector>

template<typename T>
class Stack
{
    std::vector<T> elems;

public:
    Stack() = default;

    Stack(T elem)
        : elems{std::move(elem)}
    {
    }
};

During deduction for a by-value parameter, an array argument undergoes the standard array-to-pointer conversion:

Stack stack{"hello"}; // Stack<const char*>

This declaration is valid, but the stack now stores pointers rather than strings. It does not own the referenced character data. Although a string literal has static storage duration, the same stack type could later receive pointers to shorter-lived buffers.

A Deduction Guide for Owning Strings

A user-defined deduction guide can map a particular input form to a different specialization. To store string literals as owning strings, declare the following guide after the class template definition:

Stack(const char*) -> Stack<std::string>;

The parameter list on the left participates in overload resolution. The type on the right is the specialization selected when this guide wins.

Complete owning-string example

#include <string>
#include <utility>
#include <vector>

template<typename T>
class Stack
{
    std::vector<T> elems;

public:
    Stack() = default;

    Stack(T elem)
        : elems{std::move(elem)}
    {
    }

    const T& top() const
    {
        return elems.back();
    }
};

Stack(const char*) -> Stack<std::string>;

int main()
{
    Stack integers{42};   // Stack<int>
    Stack text{"hello"};  // Stack<std::string>
}

For text, the guide selects Stack<std::string>. The compiler then initializes an object of that specialization by calling its constructor. Converting "hello" to std::string happens while initializing the constructor parameter, not while applying the deduction guide.

The distinction becomes visible when different initialization syntaxes are used:

Stack invalid = "hello"; // deduction succeeds, initialization fails
Stack valid{"hello"};    // Stack<std::string>

For invalid, the guide can still determine Stack<std::string>. However, copy-initialization would require two user-defined conversions in one conversion sequence:

const char* -> std::string -> Stack<std::string>

That sequence is not permitted. With direct list-initialization, the compiler directly selects the Stack<std::string> constructor, and only the conversion of the argument to std::string is required.

Copy Deduction

The set of CTAD candidates includes a special copy deduction candidate. It allows a specialization to be deduced from another object of the same class template:

Stack first{"hello"}; // Stack<std::string>

Stack second{first};   // Stack<std::string>
Stack third(first);    // Stack<std::string>
Stack fourth = {first}; // Stack<std::string>

The type of first already contains the template argument std::string. The copy deduction candidate therefore selects the same specialization for the new object. The selected object must still be copy-constructible or otherwise validly initialized.

The existence of this candidate is specified in [over.match.class.deduct], alongside the candidates formed from constructors and user-defined deduction guides.

Templated Aggregates: C++17 and C++20

Class templates can also be aggregates. Consider a simple structure that stores a value and a comment:

#include <string>

template<typename T>
struct ValueWithComment
{
    T value;
    std::string comment;
};

The CTAD rules for aggregates differ between C++17 and C++20:

Standard Deduction for this aggregate
C++17 Explicit template arguments or a user-defined deduction guide are required.
C++20 The compiler can form an aggregate deduction candidate from the aggregate elements.

In C++17, a guide can provide the missing deduction rule:

template<typename T>
ValueWithComment(T, const char*)
    -> ValueWithComment<T>;

ValueWithComment item{42, "initial value"};
// ValueWithComment<int>

The guide selects ValueWithComment<int>. The two members of that specialization are then initialized from 42 and "initial value".

A guide can also deliberately change the type selected for an argument:

ValueWithComment(const char*, const char*)
    -> ValueWithComment<std::string>;

ValueWithComment item{"hello", "initial value"};
// ValueWithComment<std::string>

Since C++20, the simple integer example can use automatic aggregate deduction:

ValueWithComment item{42, "initial value"};
// C++20: ValueWithComment<int>

The compiler forms an aggregate deduction candidate when the class satisfies the relevant aggregate requirements, the initializer is suitable, and no user-defined deduction guides have been declared for the class template. More complex aggregates can be subject to additional restrictions involving base classes, arrays, and parameter packs.

Key Takeaways

  • CTAD was introduced in C++17 and allows a class template specialization to be selected from an initializer.
  • Constructors produce implicit deduction candidates with matching parameter lists.
  • The return type of the candidate selected by overload resolution replaces the class template placeholder.
  • After deduction, the selected specialization must still be initialized successfully.
  • A deduction guide selects a specialization but is not a constructor and performs no conversion by itself.
  • Reference parameters preserve the array type of string literals, while by-value parameters apply array-to-pointer conversion.
  • A guide can map const char* to std::string when the container should own its text.
  • The copy deduction candidate transfers template arguments from another object of the same class template.
  • Aggregate CTAD requires a user-defined guide in C++17, while C++20 can generate an aggregate deduction candidate for suitable aggregates.