Coming Soon
This lesson is currently being developed
Class template argument deduction (CTAD) and deduction guides
Let the compiler deduce template arguments automatically.
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
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
-
CTAD automatically deduces template arguments from constructor parameters (C++17+).
-
Deduction guides provide custom deduction rules when automatic deduction isn't sufficient.
-
CTAD works with copy/move constructors and multiple template parameters.
-
Standard library containers support CTAD, making them easier to use.
-
Be aware of unintended type deductions, especially with numeric literals.
-
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
- What is CTAD and what C++ version introduced it?
- In what situations can the compiler automatically deduce template arguments?
- What are deduction guides and when would you use them?
- How does CTAD work with multiple template parameters?
- What are some potential pitfalls when relying on CTAD?
Practice exercises
Try these exercises with CTAD:
- Create a simple
Point<T>
template and test CTAD with different numeric types, observing how different literals affect the deduced types. - Create a template that requires a deduction guide (e.g., one that constructs from iterator pairs) and implement the appropriate guide.
- Experiment with CTAD and the standard library containers (
std::vector
,std::pair
,std::tuple
) to see how it simplifies their usage. - Create a template where CTAD might deduce an undesired type and provide a deduction guide to fix it.
Explore More Courses
Discover other available courses while this lesson is being prepared.
Browse CoursesLesson Discussion
Share your thoughts and questions