Contents
With C++17, we get class template argument deduction. It is based on template argument deduction for function templates and allows us to get rid of the need for clumsy make_XXX
functions.
The problem
Template argument deduction for function templates has been around since before the C++98 standard. It allows us to write cleaner and less verbose code. For example, in int m = std::max(22, 54);
it is pretty obvious that we are calling std::max<int>
here and not std::max<double>
or std::max<MyClass>
. In other contexts, we do not really care too much about the concrete template argument types or they might be impossible to type:
Point rightmost = *std::max_element(
std::begin(all_points),
std::end(all_points),
[](Point const& p1, Point const& p2) {
return p2.x > p1.x;
}
);
Here, we have std::max_element<Iter, Compare>
– and we don’t care what kind of iterator Iter
is, and we can’t specify the type of Comp
because we used a lambda.
With auto
we got even more capabilities for the compiler to deduce types for variables and function return types in C++11 and C++14.
However, what has been missing from the start is class template argument deduction. When we created, for example, a new std::pair
of things we had to explicitly say what kind of pair it was, e.g. std::pair<int, double> myPair(22, 43.9);
The common workaround for this problem has been to provide a make_XXX
function that uses function template argument deduction to determine the class template argument types. The above example then could be written as auto myPair = std::make_pair(22, 43.9);
However, this requires the use of a function that has a different name, which is rather clumsy. Authors of class templates might or might not have written those functions, and, of course, writing those functions by hand is boilerplate that brings nothing but the chance to introduce bugs.
C++17 solves the issue by introducing automated and user defined class template argument deduction. Now we can just do the above by simply writing std::pair myPair{22, 43.9};
.
How it works
The basis for class template argument deduction is, again, function template argument deduction. If an object is created using a template name, but without specifying any template parameters, the compiler builds an imaginary set of “constructor function templates” called deduction guides and uses the usual overload resolution and argument deduction rules for function templates.
Object creation may occur as shown above for the pair, or vía function style construction like myMap.insert(std::pair{"foo"s, 32});
, or in a new expression. Those deduction guides are not actually created or called – it’s only a concept for how the compiler picks the right template parameters and constructor for the creation of the object.
The set of deduction guides consists of some automatically generated ones and – optionally – some user-defined ones.
Automatic deduction guides
The compiler basically generates one deduction guide for each constructor of the primary class template. The template parameters of the imaginary constructor function template are the class template parameters plus any template parameters the constructor might have. The function parameters are used as the are. For std::pair
some of those imaginary function templates would then look like this:
template <class T1, class T2>
constexpr auto pair_deduction_guide() -> std::pair<T1, T2>;
template <class T1, class T2>
auto pair_deduction_guide(std::pair<T1, T2> const& p) -> std::pair<T1, T2>;
template <class T1, class T2>
constexpr auto pair_deduction_guide(T1 const& x, T2 const& y) -> std::pair<T1, T2>;
template <class T1, class T2, class U1, class U2>
constexpr auto pair_deduction_guide(U1&& x, U2&& y) -> std::pair<T1, T2>;
template <class T1, class T2, class U1, class U2>
constexpr auto pair_deduction_guide(std::pair<U1, U2> const& p) -> std::pair<T1, T2>;
//etc...
The first deduction guide would be the one generated from pair
‘s default constructor. The second from the copy constructor, and the third from the constructor that copies arguments of the exact right types. This is the one that makes std::make_pair
pretty much obsolete. The fourth is generated from the constructor that converts arguments to T1
and T2
and so on.
Of the four deduction guides shown, all would be generated and considered for overload resolution, but only the second and third would ever be actually used. The reason is that for the others, the compiler would not be able to deduce T1
and T2
– and explicitly providing them would turn off class argument deduction and we’re back to the old days.
There are two deduction guides that may be generated even if the corresponding constructor does not exist: If the primary template does not have any constructors or is not defined at all, a deduction guide for what would be the default constructor is generated. In addition, the compiler will always generate a copy deduction guide. The latter makes sense if you think about a class similar to this:
template <class T>
struct X {
T t;
X(T const& t_) : t{t_} {}
};
X x{22}; // -> X<int>
X x2{x};
Without the copy deduction guide, there could be cases where x2
would not be deduced as a copy of x
which it obviously should be, but as a X<X<int>>
, wrapping a copy of x
.
Note: Automatic deduction guides are only generated for constructors of the primary template. That means if you have partial or full template specializations that provide additional constructors, they will not be considered. If you want to add them to the set of deduction guides, you have to write them manually.
User-defined deduction guides
User defined deduction guides have to be defined in the same scope as the class template they apply to. They look pretty similar to the pseudo code I wrote above for the automatic guides. A user-defined version of the deduction guide that replaces make_pair
would have to be written like this:
namespace std {
// ...
template<class T1, class T2>
pair(T1 const&, T2 const&) -> pair<T1, T2>;
}
They look pretty much like a function signature with trailing return type, but without the auto
return type – which could be considered consistent with the syntax of constructors which don’t have a return type either.
There is not much more surprising to user-defined deduction guides. We can’t write a function body since they are not actual functions but only hints which constructor of which class template instantiation to call. One thing to note is that they don’t need to be templates. For example, the following guide could make sense:
template <class T>
class Element {
//...
public:
Element(T const&);
};
//don't wrap C-strings in Elements...
Element(char const*) -> Element<std::string>;
A popular example for user-defined deduction guides are range constructors for standard containers, e.g. std::set
:
template <class Iter>
std::set<T, Allocator>::set(Iterfirst, Iterlast, Allocator const& alloc = Allocator());
The automatic deduction guide for this constructor will not work since the compiler can not deduce T
. With user-defined deduction guides, the standard library can help out. It will look something like this:
template <class Iter, class Allocator>
set(Iter, Iter, Allocator const&) -> set<typename std::iterator_traits<Iter>::value_type, Allocator>;
The C++17 standard library provides a lot of sensible deduction guides like this one.
Conclusion
With class template argument deduction, the C++17 standard closes a gap in our toolbox to write simple, yet type-safe code. The need for make_XXX
workaround functions is gone (this does not apply to make_unique
and make_shared
which do something different).
How often should we rely on class template argument deduction? Time will tell what the best practices are, but my guess is that it will be similar to template argument deduction for functions: Use it by default, only explicitly specify template parameters when they can’t be deduced or when not using them would make the code unclear.
Permalink
Permalink
Permalink
“the compiler will always generate a copy deduction guide”
Is there a move deduction guide too?
Permalink
No. The actual deduction guide could be thought of as
template <class... Ts>
X(X<Ts...>) -> X<Ts...>
Without any ref on the argument. The actual constructor call will then be something different and be resolved according to the actual arguments.
Permalink
Sounds like that (almost) makes std::experimental::make_array() obsolete also.
Permalink
Yes.
std::array
now has a “user”-defined deduction rule which will give anstd::array<std::common_type<Args...>>
(modulo decaying the arguments, but you get the gist)Permalink
Beware that there are cases when std::make_pair and std::pair with the same arguments actually create two different types of a pair. See this SO answer for more details: https://stackoverflow.com/a/43875608/2580955
Permalink
What is it that
make_unique
andmake_shared
do differently?Permalink
Both allocate memory and do not deduce the return type. In fact, you have to explicitly provide the template parameter of the returned smart pointer.
Permalink
Is it not also the case that they are more exception-safe? I’m not clear on the details (I don’t use exceptions much), but I heard that as reasoning 🙂
Permalink
The exception safety comes mostly from the smart pointer they return, but they tie the memory allocation and binding that memory to the smarty pointer into a single step, so you don’t have to do ity manually.