Modern C++ Features – Class Template Argument Deduction

With C++17, we get class template argument deduction. It is based on template argument deduction for function templates and allows us to get rid of the need for clumsy make_XXX functions.

The problem

Template argument deduction for function templates has been around since before the C++98 standard. It allows us to write cleaner and less verbose code. For example, in int m = std::max(22, 54); it is pretty obvious that we are calling std::max<int> here and not std::max<double> or std::max<MyClass>. In other contexts, we do not really care too much about the concrete template argument types or they might be impossible to type:

Point rightmost = *std::max_element(
  std::begin(all_points), 
  std::end(all_points), 
  [](Point const& p1, Point const& p2) { 
    return p2.x > p1.x; 
  }

);

Here, we have std::max_element<Iter, Compare> – and we don’t care what kind of iterator Iter is, and we can’t specify the type of Comp because we used a lambda.

With auto we got even more capabilities for the compiler to deduce types for variables and function return types in C++11 and C++14.

However, what has been missing from the start is class template argument deduction. When we created, for example, a new std::pair of things we had to explicitly say what kind of pair it was, e.g. std::pair<int, double> myPair(22, 43.9);

The common workaround for this problem has been to provide a make_XXX function that uses function template argument deduction to determine the class template argument types. The above example then could be written as auto myPair = std::make_pair(22, 43.9);

However, this requires the use of a function that has a different name, which is rather clumsy. Authors of class templates might or might not have written those functions, and, of course, writing those functions by hand is boilerplate that brings nothing but the chance to introduce bugs.

C++17 solves the issue by introducing automated and user defined class template argument deduction. Now we can just do the above by simply writing std::pair myPair{22, 43.9};.

How it works

The basis for class template argument deduction is, again, function template argument deduction. If an object is created using a template name, but without specifying any template parameters, the compiler builds an imaginary set of “constructor function templates” called deduction guides and uses the usual overload resolution and argument deduction rules for function templates.

Object creation may occur as shown above for the pair, or vía function style construction like myMap.insert(std::pair{"foo"s, 32});, or in a new expression. Those deduction guides are not actually created or called – it’s only a concept for how the compiler picks the right template parameters and constructor for the creation of the object.

The set of deduction guides consists of some automatically generated ones and – optionally – some user-defined ones.

Automatic deduction guides

The compiler basically generates one deduction guide for each constructor of the primary class template. The template parameters of the imaginary constructor function template are the class template parameters plus any template parameters the constructor might have. The function parameters are used as the are. For std::pair some of those imaginary function templates would then look like this:

template <class T1, class T2>
constexpr auto pair_deduction_guide() -> std::pair<T1, T2>;

template <class T1, class T2>
auto pair_deduction_guide(std::pair<T1, T2> const& p) -> std::pair<T1, T2>;

template <class T1, class T2>
constexpr auto pair_deduction_guide(T1 const& x, T2 const& y) -> std::pair<T1, T2>;

template <class T1, class T2, class U1, class U2>
constexpr auto pair_deduction_guide(U1&& x, U2&& y) -> std::pair<T1, T2>;

template <class T1, class T2, class U1, class U2>
constexpr auto pair_deduction_guide(std::pair<U1, U2> const& p) -> std::pair<T1, T2>;

//etc...

The first deduction guide would be the one generated from pair‘s default constructor. The second from the copy constructor, and the third from the constructor that copies arguments of the exact right types. This is the one that makes std::make_pair pretty much obsolete. The fourth is generated from the constructor that converts arguments to T1 and T2 and so on.

Of the four deduction guides shown, all would be generated and considered for overload resolution, but only the second and third would ever be actually used. The reason is that for the others, the compiler would not be able to deduce T1 and T2 – and explicitly providing them would turn off class argument deduction and we’re back to the old days.

There are two deduction guides that may be generated even if the corresponding constructor does not exist: If the primary template does not have any constructors or is not defined at all, a deduction guide for what would be the default constructor is generated. In addition, the compiler will always generate a copy deduction guide. The latter makes sense if you think about a class similar to this:

template <class T>
struct X {
  T t;
  X(T const& t_) : t{t_} {}
};

X x{22}; // -> X<int>
X x2{x};

Without the copy deduction guide, there could be cases where x2 would not be deduced as a copy of x which it obviously should be, but as a X<X<int>>, wrapping a copy of x.

Note: Automatic deduction guides are only generated for constructors of the primary template. That means if you have partial or full template specializations that provide additional constructors, they will not be considered. If you want to add them to the set of deduction guides, you have to write them manually.

User-defined deduction guides

User defined deduction guides have to be defined in the same scope as the class template they apply to. They look pretty similar to the pseudo code I wrote above for the automatic guides. A user-defined version of the deduction guide that replaces make_pair would have to be written like this:

namespace std {
  // ...

  template<class T1, class T2>
  pair(T1 const&, T2 const&) -> pair<T1, T2>;
}

They look pretty much like a function signature with trailing return type, but without the auto return type – which could be considered consistent with the syntax of constructors which don’t have a return type either.

There is not much more surprising to user-defined deduction guides. We can’t write a function body since they are not actual functions but only hints which constructor of which class template instantiation to call. One thing to note is that they don’t need to be templates. For example, the following guide could make sense:

template <class T>
class Element {
  //...
public: 
  Element(T const&);
};

//don't wrap C-strings in Elements...
Element(char const*) -> Element<std::string>; 

A popular example for user-defined deduction guides are range constructors for standard containers, e.g. std::set:

template <class Iter>
std::set<T, Allocator>::set(Iterfirst, Iterlast, Allocator const& alloc = Allocator());

The automatic deduction guide for this constructor will not work since the compiler can not deduce T. With user-defined deduction guides, the standard library can help out. It will look something like this:

template <class Iter, class Allocator>
set(Iter, Iter, Allocator const&) -> set<typename std::iterator_traits<Iter>::value_type, Allocator>;

The C++17 standard library provides a lot of sensible deduction guides like this one.

Conclusion

With class template argument deduction, the C++17 standard closes a gap in our toolbox to write simple, yet type-safe code. The need for make_XXX workaround functions is gone (this does not apply to make_unique and make_shared which do something different).

How often should we rely on class template argument deduction? Time will tell what the best practices are, but my guess is that it will be similar to template argument deduction for functions: Use it by default, only explicitly specify template parameters when they can’t be deduced or when not using them would make the code unclear.

Previous Post
Next Post
Facebooktwittergoogle_plusredditlinkedinFacebooktwittergoogle_plusredditlinkedinby feather

10 Comments



  1. “the compiler will always generate a copy deduction guide”

    Is there a move deduction guide too?

    Reply

    1. No. The actual deduction guide could be thought of as

      template <class... Ts>
      X(X<Ts...>) -> X<Ts...>
      

      Without any ref on the argument. The actual constructor call will then be something different and be resolved according to the actual arguments.

      Reply

  2. Sounds like that (almost) makes std::experimental::make_array() obsolete also.

    Reply

    1. Yes. std::array now has a “user”-defined deduction rule which will give an std::array<std::common_type<Args...>> (modulo decaying the arguments, but you get the gist)

      Reply

  3. What is it that make_unique and make_shared do differently?

    Reply

    1. Both allocate memory and do not deduce the return type. In fact, you have to explicitly provide the template parameter of the returned smart pointer.

      Reply

      1. Is it not also the case that they are more exception-safe? I’m not clear on the details (I don’t use exceptions much), but I heard that as reasoning 🙂

        Reply

        1. The exception safety comes mostly from the smart pointer they return, but they tie the memory allocation and binding that memory to the smarty pointer into a single step, so you don’t have to do ity manually.

          Reply

Leave a Reply