Coming Soon

This lesson is currently being developed

Class template argument deduction (CTAD) and deduction guides

Let the compiler deduce template arguments automatically.

Compound Types: Enums and Structs
Chapter
Beginner
Difficulty
45min
Estimated Time

What to Expect

Comprehensive explanations with practical examples

Interactive coding exercises to practice concepts

Knowledge quiz to test your understanding

Step-by-step guidance for beginners

Development Status

In Progress

Content is being carefully crafted to provide the best learning experience

Preview

Early Preview Content

This content is still being developed and may change before publication.

13.14 — Class template argument deduction (CTAD) and deduction guides

In this lesson, you'll learn about Class Template Argument Deduction (CTAD) - a C++17 feature that allows the compiler to automatically deduce template arguments from constructor parameters, making template usage more convenient and readable.

The problem before CTAD

Before C++17, you always had to explicitly specify template arguments when creating template objects:

#include <iostream>
#include <string>

template<typename T>
struct Container
{
    T value;
    
    Container(const T& v) : value(v) {}
    
    void print() const
    {
        std::cout << "Value: " << value << std::endl;
    }
};

int main()
{
    // Before C++17: explicit template arguments required
    Container<int> intContainer(42);
    Container<std::string> stringContainer(std::string("Hello"));
    Container<double> doubleContainer(3.14);
    
    intContainer.print();
    stringContainer.print();
    doubleContainer.print();
    
    return 0;
}

Output:

Value: 42
Value: Hello
Value: 3.14

This was verbose and redundant since the compiler could potentially figure out the types from the constructor arguments.

Introduction to CTAD (C++17)

Class Template Argument Deduction (CTAD) allows the compiler to automatically deduce template arguments from constructor parameters:

#include <iostream>
#include <string>

template<typename T>
struct Container
{
    T value;
    
    Container(const T& v) : value(v) {}
    
    void print() const
    {
        std::cout << "Value: " << value << std::endl;
    }
};

int main()
{
    // C++17: template arguments deduced automatically
    Container intContainer(42);           // Deduced as Container<int>
    Container stringContainer(std::string("Hello"));  // Deduced as Container<std::string>
    Container doubleContainer(3.14);      // Deduced as Container<double>
    
    intContainer.print();
    stringContainer.print();
    doubleContainer.print();
    
    return 0;
}

Output:

Value: 42
Value: Hello
Value: 3.14

The compiler automatically deduces that intContainer is Container<int>, stringContainer is Container<std::string>, and doubleContainer is Container<double>.

CTAD with multiple template parameters

CTAD works with templates that have multiple parameters:

#include <iostream>
#include <string>

template<typename T, typename U>
struct Pair
{
    T first;
    U second;
    
    Pair(const T& f, const U& s) : first(f), second(s) {}
    
    void print() const
    {
        std::cout << "(" << first << ", " << second << ")" << std::endl;
    }
};

int main()
{
    // All template arguments deduced from constructor parameters
    Pair coordinates(3, 4);              // Deduced as Pair<int, int>
    Pair nameAge("Alice", 25);           // Deduced as Pair<const char*, int>
    Pair priceDiscount(19.99, 0.15);     // Deduced as Pair<double, double>
    
    coordinates.print();
    nameAge.print();
    priceDiscount.print();
    
    return 0;
}

Output:

(3, 4)
(Alice, 25)
(19.99, 0.15)

CTAD with copy/move constructors

CTAD also works when copying or moving from existing template instances:

#include <iostream>

template<typename T>
struct Box
{
    T data;
    
    Box(const T& value) : data(value) {}
    Box(const Box& other) : data(other.data) {}  // Copy constructor
    
    void print() const
    {
        std::cout << "Box contains: " << data << std::endl;
    }
};

int main()
{
    Box original(42);           // Deduced as Box<int>
    Box copy(original);         // Deduced as Box<int> from copy constructor
    
    original.print();
    copy.print();
    
    return 0;
}

Output:

Box contains: 42
Box contains: 42

When CTAD doesn't work

CTAD has limitations. It can't deduce template arguments in certain situations:

#include <iostream>
#include <vector>

template<typename T>
struct Container
{
    std::vector<T> data;
    
    // Default constructor - no parameters to deduce from
    Container() = default;
    
    Container(const std::vector<T>& v) : data(v) {}
    
    void add(const T& item)
    {
        data.push_back(item);
    }
    
    void print() const
    {
        for (const auto& item : data)
        {
            std::cout << item << " ";
        }
        std::cout << std::endl;
    }
};

int main()
{
    // These work - template arguments can be deduced
    Container c1(std::vector<int>{1, 2, 3});  // Deduced as Container<int>
    
    // This doesn't work - no constructor arguments to deduce from
    // Container c2;  // ERROR: can't deduce T
    
    // Must explicitly specify template argument for default constructor
    Container<int> c3;
    c3.add(10);
    c3.add(20);
    
    c1.print();
    c3.print();
    
    return 0;
}

Output:

1 2 3 
10 20 

Introduction to deduction guides

Deduction guides are user-defined rules that help the compiler deduce template arguments in situations where automatic deduction isn't possible or doesn't produce the desired result:

#include <iostream>
#include <vector>
#include <string>

template<typename T>
struct SmartContainer
{
    std::vector<T> data;
    
    // Constructor from initializer list
    template<typename Iterator>
    SmartContainer(Iterator begin, Iterator end) : data(begin, end) {}
    
    // Constructor from single value (creates container with one element)
    SmartContainer(const T& singleValue) : data{singleValue} {}
    
    void print() const
    {
        for (const auto& item : data)
        {
            std::cout << item << " ";
        }
        std::cout << std::endl;
    }
};

// Deduction guide: when constructed with iterators, deduce T from iterator value type
template<typename Iterator>
SmartContainer(Iterator, Iterator) -> SmartContainer<typename std::iterator_traits<Iterator>::value_type>;

int main()
{
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    
    // Uses deduction guide to deduce SmartContainer<int>
    SmartContainer container(numbers.begin(), numbers.end());
    
    // Direct deduction from single value
    SmartContainer singleItem(42);  // Deduced as SmartContainer<int>
    
    container.print();
    singleItem.print();
    
    return 0;
}

Output:

1 2 3 4 5 
42 

Custom deduction guides

You can create custom deduction guides to control how template arguments are deduced:

#include <iostream>
#include <string>

template<typename T, typename U = T>  // U defaults to T
struct Calculator
{
    T operandA;
    U operandB;
    
    Calculator(const T& a, const U& b) : operandA(a), operandB(b) {}
    
    auto add() const
    {
        return operandA + operandB;
    }
    
    void print() const
    {
        std::cout << operandA << " + " << operandB << " = " << add() << std::endl;
    }
};

// Deduction guide: when both arguments are the same type, use that type for both T and U
template<typename T>
Calculator(const T&, const T&) -> Calculator<T, T>;

// Deduction guide: when arguments are different types, keep them separate
template<typename T, typename U>
Calculator(const T&, const U&) -> Calculator<T, U>;

int main()
{
    Calculator calc1(10, 20);        // Deduced as Calculator<int, int>
    Calculator calc2(5.5, 2);        // Deduced as Calculator<double, int>
    Calculator calc3(3.14, 2.71);    // Deduced as Calculator<double, double>
    
    calc1.print();
    calc2.print();
    calc3.print();
    
    return 0;
}

Output:

10 + 20 = 30
5.5 + 2 = 7.5
3.14 + 2.71 = 5.85

Practical example: A generic array wrapper

Here's a practical example that shows CTAD in action with a custom array wrapper:

#include <iostream>
#include <initializer_list>
#include <vector>

template<typename T>
struct Array
{
    std::vector<T> elements;
    
    // Constructor from initializer list
    Array(std::initializer_list<T> list) : elements(list) {}
    
    // Constructor from size and default value
    Array(size_t size, const T& defaultValue) : elements(size, defaultValue) {}
    
    // Constructor from another container
    template<typename Container>
    Array(const Container& container) : elements(container.begin(), container.end()) {}
    
    T& operator[](size_t index) { return elements[index]; }
    const T& operator[](size_t index) const { return elements[index]; }
    
    size_t size() const { return elements.size(); }
    
    void print() const
    {
        std::cout << "[";
        for (size_t i = 0; i < elements.size(); ++i)
        {
            std::cout << elements[i];
            if (i < elements.size() - 1) std::cout << ", ";
        }
        std::cout << "]" << std::endl;
    }
};

// Deduction guide for container constructor
template<typename Container>
Array(const Container&) -> Array<typename Container::value_type>;

int main()
{
    // CTAD from initializer list
    Array arr1{1, 2, 3, 4, 5};           // Deduced as Array<int>
    
    // CTAD from size and default value
    Array arr2(3, 42);                   // Deduced as Array<int>
    
    // CTAD from string literals
    Array arr3{"hello", "world"};        // Deduced as Array<const char*>
    
    // CTAD with existing vector (using deduction guide)
    std::vector<double> vec = {1.1, 2.2, 3.3};
    Array arr4(vec);                     // Deduced as Array<double>
    
    std::cout << "arr1: "; arr1.print();
    std::cout << "arr2: "; arr2.print();
    std::cout << "arr3: "; arr3.print();
    std::cout << "arr4: "; arr4.print();
    
    return 0;
}

Output:

arr1: [1, 2, 3, 4, 5]
arr2: [42, 42, 42]
arr3: [hello, world]
arr4: [1.1, 2.2, 3.3]

Standard library examples

Many standard library templates support CTAD:

#include <iostream>
#include <vector>
#include <pair>
#include <tuple>

int main()
{
    // std::vector with CTAD
    std::vector data{1, 2, 3, 4, 5};          // Deduced as std::vector<int>
    
    // std::pair with CTAD
    std::pair coordinates(3, 4);              // Deduced as std::pair<int, int>
    std::pair nameAge("Alice", 25);           // Deduced as std::pair<const char*, int>
    
    // std::tuple with CTAD
    std::tuple info("Bob", 30, 75000.0);      // Deduced as std::tuple<const char*, int, double>
    
    std::cout << "Vector size: " << data.size() << std::endl;
    std::cout << "Coordinates: (" << coordinates.first << ", " << coordinates.second << ")" << std::endl;
    std::cout << "Name: " << nameAge.first << ", Age: " << nameAge.second << std::endl;
    std::cout << "Info tuple size: " << std::tuple_size_v<decltype(info)> << std::endl;
    
    return 0;
}

Output:

Vector size: 5
Coordinates: (3, 4)
Name: Alice, Age: 25
Info tuple size: 3

Best practices for CTAD

1. Design constructors with CTAD in mind

template<typename T>
struct Container
{
    T value;
    
    // Good: explicit constructor parameter allows CTAD
    explicit Container(const T& v) : value(v) {}
    
    // Consider providing deduction guides for complex cases
};

2. Be careful with string literals

template<typename T>
struct StringHolder
{
    T data;
    StringHolder(const T& s) : data(s) {}
};

int main()
{
    StringHolder s1("hello");        // Deduced as StringHolder<const char*>
    StringHolder s2(std::string("hello"));  // Deduced as StringHolder<std::string>
    
    // Be aware of the difference!
    return 0;
}

3. Provide deduction guides for ambiguous cases

template<typename T>
struct Wrapper
{
    T item;
    
    template<typename U>
    Wrapper(const std::vector<U>& v) : item(v[0]) {}  // This might be confusing
};

// Clear deduction guide
template<typename U>
Wrapper(const std::vector<U>&) -> Wrapper<U>;

Common pitfalls

1. Unintended type deduction

template<typename T>
struct Number
{
    T value;
    Number(T v) : value(v) {}
};

int main()
{
    Number n(42);           // int
    Number n2(42u);         // unsigned int - different type!
    Number n3(42.0f);       // float
    Number n4(42.0);        // double
    
    // All are different types!
    return 0;
}

2. CTAD with auto

int main()
{
    auto container = Container(42);  // Type is Container<int>
    // auto helps with verbosity but you still need to know the deduced type
    
    return 0;
}

Key concepts to remember

  1. CTAD automatically deduces template arguments from constructor parameters (C++17+).

  2. Deduction guides provide custom deduction rules when automatic deduction isn't sufficient.

  3. CTAD works with copy/move constructors and multiple template parameters.

  4. Standard library containers support CTAD, making them easier to use.

  5. Be aware of unintended type deductions, especially with numeric literals.

  6. Design constructors with CTAD in mind for better usability.

Summary

Class Template Argument Deduction (CTAD) is a significant quality-of-life improvement in C++17 that makes template usage more natural and less verbose. By allowing the compiler to automatically deduce template arguments from constructor parameters, CTAD reduces boilerplate code while maintaining type safety. Deduction guides provide additional control when automatic deduction isn't sufficient or doesn't produce the desired result. Understanding CTAD helps you write more readable code and better utilize both your own templates and those from the standard library.

Quiz

  1. What is CTAD and what C++ version introduced it?
  2. In what situations can the compiler automatically deduce template arguments?
  3. What are deduction guides and when would you use them?
  4. How does CTAD work with multiple template parameters?
  5. What are some potential pitfalls when relying on CTAD?

Practice exercises

Try these exercises with CTAD:

  1. Create a simple Point<T> template and test CTAD with different numeric types, observing how different literals affect the deduced types.
  2. Create a template that requires a deduction guide (e.g., one that constructs from iterator pairs) and implement the appropriate guide.
  3. Experiment with CTAD and the standard library containers (std::vector, std::pair, std::tuple) to see how it simplifies their usage.
  4. Create a template where CTAD might deduce an undesired type and provide a deduction guide to fix it.

Continue Learning

Explore other available lessons while this one is being prepared.

View Course

Explore More Courses

Discover other available courses while this lesson is being prepared.

Browse Courses

Lesson Discussion

Share your thoughts and questions

💬

No comments yet. Be the first to share your thoughts!

Sign in to join the discussion