We have seen in class templates that we used explicit type for template instantiation for example,

template <typename T, typename U>
struct Collection
{
    T a;
    U b;
};
 
Collection<int, double> c {1, 2.1};

Compiler creates the concrete definition from the template using int and double as template types arguments. However, in C++ 17 onwards, there is a feature called Class template argument deduction (CTAD) that allows us to omit the explicit types and let compiler deduce types from initializer and instantiate the correct template.

For example, consider std::pair (defined in header file <utility>) type which is similar to Collection but stores a pair of data value of any type. We can create pair as shown below,

std::pair<int, int> p{1, 2};
 
// or
 
std::pair p{1, 2}; // works with C++17 onwards

For second case, compiler does the template type deductions from the initializer list and sees both are ints, so instantiate std::pixel<int,int>.

Does it work for Collection?

Well, we can try following and see if it works or not.

#include <iostream>
 
template <typename T, typename U>
struct Collection
{
    T a;
    U b;
};
 
template <typename T, typename U>
void printCollection(Collection<T, U> p)
{
    std::cout << "{" << p.a << ", " << p.b << "}" << "\n";
}
 
int main(int argc, char const *argv[])
{
    Collection c{1, 2};
}

When we compile with C++17, it fails and gives following compilation error on my machine.

ctad.cpp: In function 'int main(int, const char**)':
ctad.cpp:30:22: error: class template argument deduction failed:
   30 |     Collection c{1, 2};
      |                      ^
ctad.cpp:30:22: error: no matching function for call to 'Collection(int, int)'
ctad.cpp:30:22: note: there are 2 candidates
ctad.cpp:6:8: note: candidate 1: 'template<class T, class U> Collection() -> Collection<T, U>'
    6 | struct Collection
      |        ^~~~~~~~~~
ctad.cpp:6:8: note: candidate expects 0 arguments, 2 provided
ctad.cpp:6:8: note: candidate 2: 'template<class T, class U> Collection(Collection<T, U>) -> Collection<T, U>'
ctad.cpp:6:8: note: candidate expects 1 argument, 2 provided

We would see that compiler is trying to look a match for Collection(int, int) but it could not find it. So, we need to tell the compiler that when such case comes, use the provided rule. For example,

template <typename T, typename U>
Collection(T, U) -> Collection<T, U>;

With this, compiler is able to match Collection(int, int) -> Collection<int,int> and instantiates the template.

We do not have to do this for C++20 as compiler is itself able to generate deduction guides for aggregates like Collection.

Tip

Template type deduction for std::pair works with C++17 because it defines the guidelines.

Warning

CTAD does not work when initializing a non-static class type data member as shown below:

struct Foo
{
   std::pair<int,int> p1{1, 2}; // works
   std::pair p2{1, 2}; // does not work
}

CTAD does not work with function parameter

Following does not work.

void printCollection(Collection p)
{
    std::cout << "{" << p.a << ", " << p.b << "}" << "\n";
}

It does not work because CTAD works for deductions for template argument type deductions from the initialization list. It does not work with function parameters. So, this function needs to be used with function template as shown below.

template <typename T, typename U>
void printCollection(Collection<T, U> p)
{
    std::cout << "{" << p.a << ", " << p.b << "}" << "\n";
}

References

  1. https://www.learncpp.com/cpp-tutorial/class-template-argument-deduction-ctad-and-deduction-guides/