When a type is not a type

This article has been updated for C++20.

Let’s take a look at the following code sample:

template <typename T> 
struct foo
{
   foo(T) {}
};

int main()
{
   std::pair   p{ 1, "one" };
   std::vector v{ 1, 2 };
   foo         f{ 42 };
}

Here, p is a std::pair, v is a std::vector, and f is a foo. But there is no such thing as a std::vector type (nor std::pair or foo). std::vector<int> is a type, and std::vector<T> is a type template, but std::vector is just a placeholder that activates a C++17 feature called class template argument deduction (which we will call CTAD for short).

Why CTAD?

Prior to C++17, you had to explicitly specify all the class template arguments, as all of them must be known in order to instantiate the class template. Therefore, the code above would have looked like this:

std::pair<int, char const*> p{ 1, "one" };
std::vector<int>            v{ 1, 2 };
foo<int>                    f{ 42 };

Since function template argument deduction was available for a long time, the workaround in C++11 was to provide a make_xxx() function that creates instance of the class template, and use auto as placeholder for the type.

auto p = std::make_pair(1, "one");

Of course, not all the standard types have such a helper function, so it was often the case that users wrote their own make functions. Here you can see an example of a make_vector() variadic function template that creates a vector:

template <class T, class... Ts, class Allocator = std::allocator<T>>
auto make_vector(T&& first, Ts&&... args)
{
   return std::vector<std::decay_t<T>, Allocator>{ 
      std::forward<T>(first), 
      std::forward<Ts>(args)... };
}

auto v = make_vector(1, 2);

Here is another example for the user-defined class foo:

template <typename T>
constexpr foo<T> make_foo(T&& value)
{
   return foo{ value };
}

auto f = make_foo(42);

How it works

When the compiler encounters a declaration of a variable, or a function-style cast using the name of a class template, it builds a set of deduction guides, which is basically fictional functions template representing constructor signatures of a hypothetical class type. These implicit deduction guides created by the compiler can be complemented with user-defined deduction guides. They are then used for performing template argument deduction and overload resolution for initializing objects of this hypothetical class.

Here are several examples (not the complete list) for the implicit deduction types that the compiler constructs for the std::pair class template:

template <class T1, class T2>
std::pair<T1, T2> F();

template <class T1, class T2>
std::pair<T1, T2> F(T1 const& x, T2 const& y);

template <class T1, class T2, class U1, class U2>
std::pair<T1, T2> F(U1&& x, U2&& y);

The implicit deduction guides are generated from the constructors of the class template (the default constructor, the copy constructor, and all the other constructor with the type arguments copied in their exact order). If the class template does not have any constructor, a deduction guide is created for a hypothetical default constructor. In any case, a deduction guide for a hypothetical copy constructor is created.

User-defined deduction guides are very similar to function signature with trailing return type but without the auto keyword (after all, they represent fictional constructor signatures). They must be defined in the scope of the class template they apply to. So an example for std::pair could be (although this is actually provided implicitly by the compiler):

namespace std {
   template <class T1, class T2>
   pair(T1&& v1, T2&& v2)->pair<T1, T2>;
}

Consider the following class type bar that has a constructor using iterators:

template <typename T>
struct bar
{
   template <class Iter>
   bar(Iter first, Iter last) {}
};

The idea is to be able to initialize objects of this type template as follows:

int arr[] = { 1,2,3,4,5 };
bar b{std::begin(arr), std::end(arr)};

However, this does not work. For instance, the VC++ compiler generates the following errors:

error C2672: 'bar': no matching overloaded function found
error C2783: 'bar<T> bar(Iter,Iter)': could not deduce template argument for 'T'

These can be fixed using a user-defined deduction guide as shown below:

template <class Iter>
bar(Iter first, Iter last)->bar<typename std::iterator_traits<Iter>::value_type>;

The deduction guides don’t have to be templates. Considering the class foo from above, we can have the following deduction guide that forces the compiler to always create instances of foo<std::string> when a char const* is used as argument.

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

This example can be further applied on the std::pair class template, so that std::string is always used instead of char const*:

namespace std {
   template <class T>
   pair(T&&, char const *)->pair<T, std::string>;

   template <class T>
   pair(char const *, T&&)->pair<std::string, T>;

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

std::pair  p1{ 1, "one" };    // std::pair<int, std::string>
std::pair  p2{ "one", 1 };    // std::pair<std::string, int>
std::pair  p3{ "1", "one" };  // std::pair<std::string, std::string>

Gotchas

CTAD does not take place when the template argument list is present. The following two declarations are both legal:

std::pair<int, std::string> p1 {1, "one"};
std::pair                   p2 {1, "one"};

However, none of the following is valid, as CTAD does not take place:

std::pair<>    p1 { 1, "one" };
std::pair<int> p2 { 1, "one" };

If you have aggregate types that you want to initialize taking advantage of CTAD then you probably need to define your own deduction guides. Let’s consider the following class template foo. Initializing objects without providing the template argument list does not work.

template <typename T> 
struct foo
{
   T t;
};

foo f{ 42 };  // error: no matching overloaded function found

To leverage CTAD you need to define your own deduction guide, which in this case is as follows:

template <typename T>
foo(T)->foo<T>;

C++20 support for aggregate types

The C++20 standard has added support for aggregate types. Defining a deduction rule as mentioned above is no longer needed for aggregates, provided that:

  • the variable is initialized from a non-empty list of variables
  • any dependent base class has no virtual functions or virtual base classes

Therefore, in C++20 the previous example with the foo<T> class template no longer requires your own deduction guide.

Conclusions

Class template argument deduction is a useful feature in C++17 that helps developers to simplify the code by avoiding writing the template argument list when initializing objects of class templates (or when performing function-style casts). The compiler provides an implicit set of deduction guides, which are fictional function templates for a hypothetical class and uses them to perform template argument deduction and overload resolution. However, you can extend this set of deduction guides with your own, and in some cases, such as for aggregate types, you need to do so.

See also

You can learn more about this feature from the following articles:

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.