23 January, 2022

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

Decay

Books