Skip to content

Last updated:2026-10-12

C++17 Class Template Argument Deduction CTAD

Introduction

C++ template metaprogramming has long been a well-known capability among C++ developers: newcomers and advanced developers alike can use it to write generic code with type parameters. With class template argument deduction (CTAD) introduced in C++17, working with class templates has become more intuitive and flexible.

All the code in this section will also be updated in the community.

CTAD, introduced in C++17, brings the following benefits:

For example, with such complicated code

std::vector<FooBar<int, const char*>> obj{a, b, c}; 

we only need:

 std::vector obj{a, b, c};

Or again:

std::tuple<int>

can be simplified to:

std::tuple t1{1};

So today let's take a look at CTAD.

CTAD stands for Class template argument deduction (CTAD), that is, class template argument deduction. Once you give the compiler a deduction guide, we can use this feature.

If we do not provide one, as in the example below:

#include <iostream>
template <typename T, typename U>
struct MyPair {
  T first{};
  U second{};
};

int main() {
  MyPair<int, int> p1{1, 2};
  MyPair p2{1, 2};
  std::cout << p1.first << ", " << p1.second << std::endl;
  std::cout << p2.first << ", " << p2.second << std::endl;
  return 0;
}

For p2 we get an error:

no viable constructor or deduction guide for deduction of template arguments of 'MyPair'

So how do we fix it?

We only need to add a deduction guide, written as follows.

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

Class template argument deduction (CTAD) simplifies the instantiation of class templates by allowing the compiler to deduce template arguments from constructor arguments. Before CTAD was introduced, developers had to specify template arguments explicitly when instantiating. With CTAD, such explicit specification is no longer necessary, making the code easier to read and maintain.

template <typename T>
class Add{
    private:
        T first;
        T second;
    public:
        Add() = default;
        Add(T first, T second): first_{first}, second_{second} {}
        T result() const { return first + second; }
};

int main(){
    Add one(1,2);               // T被推导为int
    Add two{1.245, 3.1415};     // T被推导为double
    Add three = {0.24f, 0.34f}; // T被推导为float
}

In the examples above, CTAD allows the type T to be deduced based on the constructor arguments, eliminating the need for explicit type specification.

Non-static member initialization

But there is one scenario where CTAD cannot be used: non-static member initialization. For example, to use the outer MyPair inside a Test class, you must write the template arguments out in full.

template <typename T = int, typename U = int>
struct MyPair {
  T first{};
  U second{};
};

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

class Test {
  MyPair p4;   // use of class template 'MyPair' requires template arguments; argument
               // deduction not allowed in non-static class member
};

In the example above, we defined a template class MyPair and used CTAD with it. However, when we try to use MyPair as a non-static member of the class Test, CTAD no longer works because of the non-static member initialization context, resulting in a compilation error. In this case, we must explicitly specify the template arguments of MyPair.

As we can see, in the context of non-static member initialization CTAD cannot deduce template arguments. Therefore, when a class template is used as a non-static member, developers must explicitly specify all the template arguments. This is something to pay special attention to in real-world development, to ensure the correctness and maintainability of the code.

Comments