How to Use Class Template Argument Deduction

Stephan T. Lavavej - MSFT

Class Template Argument Deduction (CTAD) is a C++17 Core Language feature that reduces code verbosity. C++17’s Standard Library also supports CTAD, so after upgrading your toolset, you can take advantage of this new feature when using STL types like std::pair and std::vector. Class templates in other libraries and your own code will partially benefit from CTAD automatically, but sometimes they’ll need a bit of new code (deduction guides) to fully benefit. Fortunately, both using CTAD and providing deduction guides is pretty easy, despite template metaprogramming’s fearsome reputation!

CTAD support is available in VS 2017 15.7 and later with the /std:c++17 and /std:c++latest compiler options.

Template Argument Deduction

C++98 through C++14 performed template argument deduction for function templates. Given a function template like template <typename RanIt> void sort(RanIt first, RanIt last);, you can and should sort a std::vector<int> without explicitly specifying that RanIt is std::vector<int>::iterator. When the compiler sees sort(v.begin(), v.end());, it knows what the types of v.begin() and v.end() are, so it can determine what RanIt should be. The process of determining template arguments for template parameters (by comparing the types of function arguments to function parameters, according to rules in the Standard) is known as template argument deduction, which makes function templates far more usable than they would otherwise be.

However, class templates didn’t benefit from these rules. If you wanted to construct a std::pair from two ints, you had to say std::pair<int, int> p(11, 22);, despite the fact that the compiler already knows that the types of 11 and 22 are int. The workaround for this limitation was to use function template argument deduction: std::make_pair(11, 22) returns std::pair<int, int>. Like most workarounds, this is problematic for a few reasons: defining such helper functions often involves template metaprogramming (std::make_pair() needs to perform perfect forwarding and decay, among other things), compiler throughput is reduced (as the front-end has to instantiate the helper, and the back-end has to optimize it away), debugging is more annoying (as you have to step through helper functions), and there’s still a verbosity cost (the extra make_ prefix, and if you want a local variable instead of a temporary, you need to say auto).

Hello, CTAD World

C++17 extends template argument deduction to the construction of an object given only the name of a class template. Now, you can say std::pair(11, 22) and this is equivalent to std::pair<int, int>(11, 22). Here’s a full example, with a C++17 terse static_assert verifying that the declared type of p is the same as std::pair<int, const char *>:

C:\Temp>type meow.cpp
#include <type_traits>
#include <utility>
int main() {
    std::pair p(1729, "taxicab");
    static_assert(std::is_same_v<decltype(p), std::pair<int, const char *>>);
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 meow.cpp
meow.cpp
C:\Temp>

CTAD works with parentheses and braces, and named variables and nameless temporaries.

Another Example: array and greater

C:\Temp>type arr.cpp
#include <algorithm>
#include <array>
#include <functional>
#include <iostream>
#include <string_view>
#include <type_traits>
using namespace std;
int main() {
    array arr = { "lion"sv, "direwolf"sv, "stag"sv, "dragon"sv };
    static_assert(is_same_v<decltype(arr), array<string_view, 4>>);
    sort(arr.begin(), arr.end(), greater{});
    cout << arr.size() << ": ";
    for (const auto& e : arr) {
        cout << e << " ";
    }
    cout << "\n";
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 arr.cpp && arr
arr.cpp
4: stag lion dragon direwolf

This demonstrates a couple of neat things. First, CTAD for std::array deduces both its element type and its size. Second, CTAD works with default template arguments; greater{} constructs an object of type greater<void> because it’s declared as template <typename T = void> struct greater;.

CTAD for Your Own Types

C:\Temp>type mypair.cpp
#include <type_traits>
template <typename A, typename B> struct MyPair {
    MyPair() { }
    MyPair(const A&, const B&) { }
};
int main() {
    MyPair mp{11, 22};
    static_assert(std::is_same_v<decltype(mp), MyPair<int, int>>);
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 mypair.cpp
mypair.cpp
C:\Temp>

In this case, CTAD automatically works for MyPair. What happens is that the compiler sees that a MyPair is being constructed, so it runs template argument deduction for MyPair‘s constructors. Given the signature (const A&, const B&) and the arguments of type int, A and B are deduced to be int, and those template arguments are used for the class and the constructor.

However, MyPair{} would emit a compiler error. That’s because the compiler would attempt to deduce A and B, but there are no constructor arguments and no default template arguments, so it can’t guess whether you want MyPair<int, int> or MyPair<Starship, Captain>.

Deduction Guides

In general, CTAD automatically works when class templates have constructors whose signatures mention all of the class template parameters (like MyPair above). However, sometimes constructors themselves are templated, which breaks the connection that CTAD relies on. In those cases, the author of the class template can provide “deduction guides” that tell the compiler how to deduce class template arguments from constructor arguments.

C:\Temp>type guides.cpp
#include <iterator>
#include <type_traits>
template <typename T> struct MyVec {
    template <typename Iter> MyVec(Iter, Iter) { }
};
template <typename Iter> MyVec(Iter, Iter) -> MyVec<typename std::iterator_traits<Iter>::value_type>;
template <typename A, typename B> struct MyAdvancedPair {
    template <typename T, typename U> MyAdvancedPair(T&&, U&&) { }
};
template <typename X, typename Y> MyAdvancedPair(X, Y) -> MyAdvancedPair<X, Y>;
int main() {
    int * ptr = nullptr;
    MyVec v(ptr, ptr);
    static_assert(std::is_same_v<decltype(v), MyVec<int>>);
    MyAdvancedPair adv(1729, "taxicab");
    static_assert(std::is_same_v<decltype(adv), MyAdvancedPair<int, const char *>>);
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 guides.cpp
guides.cpp
C:\Temp>

Here are two of the most common cases for deduction guides in the STL: iterators and perfect forwarding. MyVec resembles a std::vector in that it’s templated on an element type T, but it’s constructible from an iterator type Iter. Calling the range constructor provides the type information we want, but the compiler can’t possibly realize the relationship between Iter and T. That’s where the deduction guide helps. After the class template definition, the syntax template <typename Iter> MyVec(Iter, Iter) -> MyVec<typename std::iterator_traits<Iter>::value_type>; tells the compiler “when you’re running CTAD for MyVec, attempt to perform template argument deduction for the signature MyVec(Iter, Iter). If that succeeds, the type you want to construct is MyVec<typename std::iterator_traits<Iter>::value_type>. That essentially dereferences the iterator type to get the element type we want.

The other case is perfect forwarding, where MyAdvancedPair has a perfect forwarding constructor like std::pair does. Again, the compiler sees that A and B versus T and U are different types, and it doesn’t know the relationship between them. In this case, the transformation we need to apply is different: we want decay (if you’re unfamiliar with decay, you can skip this). Interestingly, we don’t need decay_t, although we could use that type trait if we wanted extra verbosity. Instead, the deduction guide template <typename X, typename Y> MyAdvancedPair(X, Y) -> MyAdvancedPair<X, Y>; is sufficient. This tells the compiler “when you’re running CTAD for MyAdvancedPair, attempt to perform template argument deduction for the signature MyAdvancedPair(X, Y), as if it were taking arguments by value. Such deduction performs decay. If it succeeds, the type you want to construct is MyAdvancedPair<X, Y>.”

This demonstrates a critical fact about CTAD and deduction guides. CTAD looks at a class template’s constructors, plus its deduction guides, in order to determine the type to construct. That deduction either succeeds (determining a unique type) or fails. Once the type to construct has been chosen, overload resolution to determine which constructor to call happens normally. CTAD doesn’t affect how the constructor is called. For MyAdvancedPair (and std::pair), the deduction guide’s signature (taking arguments by value, notionally) affects the type chosen by CTAD. Afterwards, overload resolution chooses the perfect forwarding constructor, which takes its arguments by perfect forwarding, exactly as if the class type had been written with explicit template arguments.

CTAD and deduction guides are also non-intrusive. Adding deduction guides for a class template doesn’t affect existing code, which previously was required to provide explicit template arguments. That’s why we were able to add deduction guides for many STL types without breaking a single line of user code.

Enforcement

In rare cases, you might want deduction guides to reject certain code. Here’s how std::array does it:

C:\Temp>type enforce.cpp
#include <stddef.h>
#include <type_traits>
template <typename T, size_t N> struct MyArray {
    T m_array[N];
};
template <typename First, typename... Rest> struct EnforceSame {
    static_assert(std::conjunction_v<std::is_same<First, Rest>...>);
    using type = First;
};
template <typename First, typename... Rest> MyArray(First, Rest...)
    -> MyArray<typename EnforceSame<First, Rest...>::type, 1 + sizeof...(Rest)>;
int main() {
    MyArray a = { 11, 22, 33 };
    static_assert(std::is_same_v<decltype(a), MyArray<int, 3>>);
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 enforce.cpp
enforce.cpp
C:\Temp>

Like std::array, MyArray is an aggregate with no actual constructors, but CTAD still works for these class templates via deduction guides. MyArray‘s guide performs template argument deduction for MyArray(First, Rest...), enforcing all of the types to be the same, and determining the array’s size from how many arguments there are.

Similar techniques could be used to make CTAD entirely ill-formed for certain constructors, or all constructors. The STL itself hasn’t needed to do that explicitly, though. (There are only two classes where CTAD would be undesirable: unique_ptr and shared_ptr. C++17 supports both unique_ptrs and shared_ptrs to arrays, but both new T and new T[N] return T *. Therefore, there’s insufficient information to safely deduce the type of a unique_ptr or shared_ptr being constructed from a raw pointer. As it happens, this is automatically blocked in the STL due to unique_ptr‘s support for fancy pointers and shared_ptr‘s support for type erasure, both of which change the constructor signatures in ways that prevent CTAD from working.)

Corner Cases for Experts: Non-Deduced Contexts

Here are some advanced examples that aren’t meant to be imitated; instead, they’re meant to illustrate how CTAD works in complicated scenarios.

Programmers who write function templates eventually learn about “non-deduced contexts”. For example, a function template taking typename Identity<T>::type can’t deduce T from that function argument. Now that CTAD exists, non-deduced contexts affect the constructors of class templates too.

C:\Temp>type corner1.cpp
template <typename X> struct Identity {
    using type = X;
};
template <typename T> struct Corner1 {
    Corner1(typename Identity<T>::type, int) { }
};
int main() {
    Corner1 corner1(3.14, 1729);
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 corner1.cpp
corner1.cpp
corner1.cpp(10): error C2672: 'Corner1': no matching overloaded function found
corner1.cpp(10): error C2783: 'Corner1<T> Corner1(Identity<X>::type,int)': could not deduce template argument for 'T'
corner1.cpp(6): note: see declaration of 'Corner1'
corner1.cpp(10): error C2641: cannot deduce template argument for 'Corner1'
corner1.cpp(10): error C2514: 'Corner1': class has no constructors
corner1.cpp(5): note: see declaration of 'Corner1'

In corner1.cpp, typename Identity<T>::type prevents the compiler from deducing that T should be double.

Here’s a case where some but not all constructors mention T in a non-deduced context:

C:\Temp>type corner2.cpp
template <typename X> struct Identity {
    using type = X;
};
template <typename T> struct Corner2 {
    Corner2(T, long) { }
    Corner2(typename Identity<T>::type, unsigned long) { }
};
int main() {
    Corner2 corner2(3.14, 1729);
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 corner2.cpp
corner2.cpp
corner2.cpp(11): error C2668: 'Corner2<double>::Corner2': ambiguous call to overloaded function
corner2.cpp(7): note: could be 'Corner2<double>::Corner2(double,unsigned long)'
corner2.cpp(6): note: or       'Corner2<double>::Corner2(T,long)'
        with
        [
            T=double
        ]
corner2.cpp(11): note: while trying to match the argument list '(double, int)'

In corner2.cpp, CTAD succeeds but constructor overload resolution fails. CTAD ignores the constructor taking (typename Identity<T>::type, unsigned long) due to the non-deduced context, so CTAD uses only (T, long) for deduction. Like any function template, comparing the parameters (T, long) to the argument types double, int deduces T to be double. (int is convertible to long, which is sufficient for template argument deduction; it doesn’t demand an exact match there.) After CTAD has determined that Corner2<double> should be constructed, constructor overload resolution considers both signatures (double, long) and (double, unsigned long) after substitution, and those are ambiguous for the argument types double, int (because int is convertible to both long and unsigned long, and the Standard doesn’t prefer either conversion).

Corner Cases for Experts: Deduction Guides Are Preferred

C:\Temp>type corner3.cpp
#include <type_traits>
template <typename T> struct Corner3 {
    Corner3(T) { }
    template <typename U> Corner3(U) { }
};
#ifdef WITH_GUIDE
    template <typename X> Corner3(X) -> Corner3<X *>;
#endif
int main() {
    Corner3 corner3(1729);
#ifdef WITH_GUIDE
    static_assert(std::is_same_v<decltype(corner3), Corner3<int *>>);
#else
    static_assert(std::is_same_v<decltype(corner3), Corner3<int>>);
#endif
}
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 corner3.cpp
corner3.cpp
C:\Temp>cl /EHsc /nologo /W4 /std:c++17 /DWITH_GUIDE corner3.cpp
corner3.cpp
C:\Temp>

CTAD works by performing template argument deduction and overload resolution for a set of deduction candidates (hypothetical function templates) that are generated from the class template’s constructors and deduction guides. In particular, this follows the usual rules for overload resolution with only a couple of additions. Overload resolution still prefers things that are more specialized (N4713 16.3.3 [over.match.best]/1.7). When things are equally specialized, there’s a new tiebreaker: deduction guides are preferred (/1.12).

In corner3.cpp, without a deduction guide, the Corner3(T) constructor is used for CTAD (whereas Corner3(U) isn’t used for CTAD because it doesn’t mention T), and Corner3<int> is constructed. When the deduction guide is added, the signatures Corner3(T) and Corner3(X) are equally specialized, so paragraph /1.12 steps in and prefers the deduction guide. This says to construct Corner3<int *> (which then calls Corner3(U) with U = int).

Reporting Bugs

Please let us know what you think about VS. You can report bugs via the IDE’s Report A Problem and also via the web: go to the VS Developer Community and click on the C++ tab.

0 comments

Discussion is closed.

Feedback usabilla icon