Prior to C++17, class templates could not deduce their template arguments from constructor calls. Developers had to write verbose explicit types or rely on factory helper functions (make_pair, make_tuple, etc.):
// Before C++17:
std::pair<int, double> p1(42, 3.14);
auto p2 = std::make_pair(42, 3.14); // Trick: function templates always deduced types
// Starting with C++17 (CTAD):
std::pair p(42, 3.14); // Compiler deduces std::pair<int, double> on its own
CTAD (Class Template Argument Deduction) is a language mechanism that maps object construction into standard function template argument deduction and overload resolution.
1. Mechanics: How the compiler works under the hood
When the compiler encounters class template instantiation without explicit <...> angle brackets:
- It takes each existing constructor of the class (including copy and move constructors).
- Synthesizes a fictitious function template candidate from each constructor — an implicit deduction guide.
- Appends all explicit deduction guides written by the programmer or provided by the standard library.
- Performs standard Overload Resolution on this synthesized candidate set.
- Produces the target specialized class type from the winning candidate.
Example of an implicit guide:
template <typename T>
struct Holder {
T value;
Holder(T val); // constructor
};
// The compiler automatically synthesizes a fictitious candidate:
// template <typename T>
// Holder<T> __Holder_guide(T val);
When writing Holder h(10);, the compiler substitutes 10 into the synthesized guide, deduces T = int, and instantiates Holder<int>.
2. Why explicit Deduction Guides are needed
Implicit deduction guides are not always sufficient. In many scenarios, constructor parameters do not directly match class template parameters, or constructors accept indirect types (such as iterators).
The syntax for an explicit deduction guide resembles a function signature with a trailing return type and no body:
template <...>
ClassName(Parameters) -> ClassName<DeducedTypes>;
Classic example: Iterator-pair constructor (std::vector)
In std::vector, the range constructor accepts two iterators:
template <typename T>
class vector {
public:
template <typename InputIt>
vector(InputIt first, InputIt last);
};
Without an explicit deduction guide, writing:
std::vector v(iter_begin, iter_end);
would fail: element type T does not appear anywhere in the constructor arguments (only InputIt). The compiler cannot infer that the vector should contain typename std::iterator_traits<InputIt>::value_type.
Therefore, the standard library specifies an explicit deduction guide:
template <typename InputIt>
vector(InputIt, InputIt)
-> vector<typename std::iterator_traits<InputIt>::value_type>;
Now std::vector v(list.begin(), list.end()) inspects what the iterators point to and instantiates the correct vector type.
3. The tricky edge case: Copy prevention (Vector of Vectors)
CTAD features a special rule favoring copy and move constructors that can lead to surprising behavior:
std::vector v1{1, 2, 3}; // std::vector<int>
std::vector v2{v1}; // Is this std::vector<int> or std::vector<std::vector<int>>?
- The answer is
std::vector<int>(a copy ofv1), not a 1-element vector containing another vector. The synthesized guide for the copy constructor always takes precedence when a single argument of the matching class type is passed.
However, if multiple arguments are supplied:
std::vector v3{v1, v1}; // std::vector<std::vector<int>> (initializer list guide matches)
4. What improved in C++20
C++17 had noticeable gaps in CTAD, which C++20 addressed:
1. CTAD for Aggregate Types (C++20)
In C++17, CTAD required at least one explicitly declared constructor. Plain aggregate structs without constructors did not support CTAD:
template <typename T, typename U>
struct Pair {
T first;
U second;
// No constructors declared — pure aggregate
};
// In C++17: COMPILATION ERROR (required Pair<int, double>{1, 2.0})
// In C++20: WORKS SEAMLESSLY!
Pair p{1, 2.0}; // Pair<int, double>
In C++20, the compiler automatically synthesizes deduction guides for aggregate member initialization.
2. CTAD for Alias Templates (C++20)
In C++17, CTAD could not be used through template type aliases (using):
template <typename T>
using StringMap = std::map<std::string, T>;
// C++17: compilation error
// C++20: works!
StringMap m{ {"apple", 1}, {"banana", 2} }; // deduces StringMap<int>
3. Concept Constraints on Guides
Deduction guides can now be directly constrained using requires clauses or concepts:
template <std::integral T>
MyContainer(T) -> MyContainer<long long>;
Summary Checklist for CTAD Rules
- No magic: CTAD simply turns constructors into candidate function templates and runs standard Overload Resolution.
- Pass-by-value decay: If a constructor accepts
Tby value, argument decay occurs (arrays decay to pointers;"abc"yieldsconst char*, not a character array). - Mismatched types: When constructor arguments differ from class template parameters, write a custom
MyType(...) -> MyType<...>;. - In C++20: Feel free to use CTAD with aggregate structs and alias templates.