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*tostd::stringwhen 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.