A brief look at CTAD and deduction guides.
CTAD and Deduction Guides in C++
What problem does CTAD solve?
What problem does CTAD solve? The clue is in the longer version of the name, Class Template Argument Deduction.
Let’s look at an example. Take the following class template…
template<typename T>
class Greeter
{
public:
explicit Greeter(T greeting) : greeting_{greeting}
{
}
void Greet()
{
std::cout << greeting_ << '\n';
}
private:
T greeting_;
};
In C++14, to create an instance of this class you would have to spell out the template arguments, as shown below. This is verbose and annoying.
Greeter<const char*> greeter1{"Hello, world!"};
Greeter<std::string> greeter2{std::string{"Hello, world!"}};
In C++17, you can omit the template arguments as the compiler is able to deduce them. The feature that makes this possible is known as CTAD or Class Template Argument Deduction.
Greeter greeter1{"Hello, world!"};
Greeter greeter2{std::string{"Hello, world!"}};
Deduction Guides
What if the class’s constructor took its arguments by reference rather than by value? It certainly isn’t unusual to take arguments by const reference.
template<typename T>
class Greeter
{
public:
explicit Greeter(const T& greeting) : greeting_{greeting}
{
}
// Omitted for brevity.
};
Passing a std::string
by reference works just fine.
Greeter greeter{std::string{greeting}};
But passing a string literal won’t compile. You’ll get an error such as: Cannot initialize an array element of type 'char' with an lvalue of type 'const char [14]'
.
Greeter greeter{"Hello, world!"};
The issue here is that arguments passed by reference do not decay. In the earlier examples, the arguments were passed by value and so they decayed. But in this example, the arguments are passed by reference and so don’t decay.
In concrete terms, with the example that passes by value we called the constructor with the
string literal "Hello, world!"
. Its type is const char [14]
, so it decays to const char*
when
passed by value to Greeter
’s constructor.
However, in the version that passes by reference, the type is const char(&) [14]
. References don’t decay, so
the compiler deduces that the template argument is const char [14]
. This is almost certainly not
what we wanted.
The problem can be fixed by introducing a deduction guide.
template<typename T>
Greeter(T) -> Greeter<T>;
It looks like a function declaration with a trailing return type. But what this says to the
compiler is that type deduction should work as if the type had been passed by value. And because
it is now deduced as having been passed by value, it decays to const char*
.
Now this will compile.
Greeter greeter{"Hello, world!"};
As this is C++, there are subtleties and nuances at every corner, but that’s the basic idea. For a more thorough treatment of these topics, take a look at some of the links below.
Further reading
For more information, follow these links.
CTAD
- Class template argument deduction on cppreference.com.
- Class Template Argument Deduction for Everyone - a CppCon 2018 talk from Stephen T. Lavavej.
- What are template deduction guides in C++17? on isocpp.org.
- What are template deduction guides and when should we use them? on Stack Overflow.
Decay
- Array to pointer decay on cppreference.com.
Books
- C++17 - The Complete Guide by Nicolai M. Josuttis covers these topics (and others) very well.