Class Template Argument Deduction

The C ++ 17 standard added a new feature to the language: Class Template Argument Deduction (CTAD) . Along with new features in C ++, traditionally added new ways of shooting their own limbs. In this article we will understand what CTAD is, what it is used for, how it simplifies life, and what pitfalls it has.

Let's start from afar

Recall what Template Argument Deduction is all about and what it is for. If you feel confident enough with C ++ templates, you can skip this section and proceed directly to the next.

Before C ++ 17, the output of template parameters applied only to function templates. When instantiating a function template, you may not explicitly specify those template arguments that can be inferred from the types of the actual function arguments. The rules for deducing are quite complicated, they are covered in the whole section 17.9.2 in the Standard [temp.deduct] (hereinafter I refer to the freely available version of the draft Standard ; in future versions, the section numbering may change, so I recommend searching by the mnemonic code specified in square brackets).

We will not analyze in detail all the intricacies of these rules; they are only needed by compiler developers. For practical use, it is enough to remember a simple rule: the compiler can independently derive the arguments of the function template, if this can be done unambiguously based on the available information. When deriving types of template parameters, standard transformations are applied as when calling a regular function ( const is discarded from literal types, arrays are reduced to pointers, function references are reduced to function pointers, etc.).

template <typename T> void func(T t) { // ... } int some_func(double d) { return static_cast<int>(d); } int main() { const int i = 123; func(i); // func<int> char arr[] = "Some text"; func(arr); // func<char *> func(some_func); // func<int (*)(double)> return 0; } 

All this simplifies the use of function templates, but, alas, is completely inapplicable to class templates. When instantiating class templates, all non-default template parameters had to be specified explicitly. In connection with this unpleasant property, a whole family of free functions with the prefix make_ appeared in the standard library: make_unique , make_shared , make_pair , make_tuple , etc.

 //  auto tup1 = std::tuple<int, char, double>(123, 'a', 40.0); //   auto tup2 = std::make_tuple(123, 'a', 40.0); 

New in C ++ 17

In the new Standard, by analogy with the parameters of function templates, the parameters of class templates are derived from the arguments of the called constructors:

 std::pair pr(false, 45.67); // std::pair<bool, double> std::tuple tup(123, 'a', 40.0); // std::tuple<int, char, double> std::less l; // std::less<void>,     std::less<> l template <typename T> struct A { A(T,T); }; auto y = new A{1, 2}; //  A<int> auto lck = std::lock_guard(mtx); // std::lock_guard<std::mutex> std::copy_n(vi1, 3, std::back_insert_iterator(vi2)); //       template <typename T> struct F { F(T); } std::for_each(vi.begin(), vi.end(), Foo([&](int i) {...})); // F<lambda> 

Immediately it is worth mentioning the CTAD restrictions that apply at the time of C ++ 17 (perhaps these restrictions will be removed in future versions of the Standard):

 template <typename X> using PairIntX = std::pair<int, X>; PairIntX p{1, true}; //   

 std::pair p{1, 5}; // OK std::pair<double> q{1, 5}; // ,   std::pair<double, int> r{1, 5}; // OK 

Also, the compiler will not be able to infer types of template parameters that are not explicitly related to the types of constructor arguments. The simplest example is a container constructor that accepts a pair of iterators:

 template <typename T> struct MyVector { template <typename It> MyVector(It from, It to); }; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //     T   It 

The type It is not directly related to T , although we developers know exactly how to get it. In order to tell the compiler how to output directly unrelated types, a new language construct appeared in C ++ 17 - the deduction guide , which we will discuss in the next section.

Dedication guides

For the example above, the deduction guide would look like this:

 template <typename It> MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; 

Here we tell the compiler that for a constructor with two parameters of the same type, you can determine the type of T using the construction std::iterator_traits<It>::value_type . Please note that deduction guides are outside the class definition, this allows you to customize the behavior of external classes, including classes from the C ++ Standard Library.

A formal description of the syntax of deduction guides is given in C ++ Standard 17 in section 17.10 [] :

 [explicit] template-name (parameter-declaration-clause) -> simple-template-id; 

The explicit keyword before the deduction guide forbids using it with copy-list-initialization :

 template <typename It> explicit MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //  MyVector v3 = {dv.begin(), dv.end()}; //   

By the way, the deduction guide does not have to be a template:

 template<class T> struct S { S(T); }; S(char const*) -> S<std::string>; S s{"hello"}; // S<std::string> 

Detailed CTAD algorithm

Formal rules for deriving class template arguments are described in detail in clause [over.match.class.deduct] of C ++ Standard 17. Let's try to figure them out.

So, we have a template type C for which CTAD is applied. In order to choose which constructor and with what parameters to call, for C , a lot of template functions are formed according to the following rules:


 template <typename T, typename U> class C { public: template <typename V, typename W = A> C(V, W); }; //    template <typename T, typename U, typename V, typename W = A> C<T, U> Fi(V, W); 


 template <typename T, typename V> C(T, V) -> C<typename DT<T>, typename DT<V>>; //    template <typename T, typename V> C<typename DT<T>, typename DT<V>> Fi(T,V); 

Further, for the resulting set of Fi dummy functions, the usual rules for outputting template parameters and overload resolution are applied with one exception: when a dummy function is called with an initialization list consisting of a single parameter of type cv U , where U is specialization C or a type inherited from specialization C (just in case, I’ll clarify that cv == const volatile ; such a record means the types U , const U , volatile U and const volatile U are treated the same way), the rule that gives priority to the constructor C(std::initializer_list<>) skipped ( for details of list initia lization can be found in clause [over.match.list] of C ++ Standard 17). Example:

 std::vector v1{1, 2}; // std::vector<int> std::vector v2{v1}; // std::vector<int>,   std::vector<std::vector<int>> 

Finally, if it was possible to choose the only most suitable dummy function, then the corresponding constructor or deduction guide is selected. If there are no suitable ones, or there are several equally suitable ones, the compiler reports an error.

Underwater rocks

CTAD is used to initialize objects, and initialization is traditionally a very confusing part of the C ++ language. With the addition of uniform initialization in C ++ 11, ways to shoot your own leg have only increased. Now you can call the constructor for an object with both round and curly brackets. In many cases, both of these options work the same, but not always:

 std::vector v1{8, 15}; // [8, 15] std::vector v2(8, 15); // [15, 15, … 15] (8 ) std::vector v3{8}; // [8] std::vector v4(8); //   

So far, everything seems to be quite logical: v1 and v3 call the constructor that takes std::initializer_list<int> , int is inferred from the parameters; v4 cannot find a constructor that takes only one parameter of type int . But these are still flowers, berries in front:

 std::vector v5{"hi", "world"}; // [“hi”, “world”] std::vector v6("hi", "world"); // ?? 

v5 , as expected, will be of type std::vector<const char*> and initialized with two elements, but the next line does something completely different. For a vector, there is only one constructor that takes two parameters of the same type:

 template< class InputIt > vector( InputIt first, InputIt last, const Allocator& alloc = Allocator() ); 

thanks to the deduction guide for std::vector "hi" and "world" will be treated as iterators, and all elements lying "between" will be added to a vector of type std::vector<char> . If we are lucky and these two string constants are in memory in a row, then three elements will fall into the vector: 'h', 'i', '\ x00', but most likely, such code will lead to a violation of memory protection and crash of the program.

Materials used

Draft Standard C ++ 17
CppCon 2018: Stephan T. Lavavej "Class Template Argument Deduction for Everyone"


All Articles