foonathan::blog()

Thoughts from a C++ library developer.

Function templates - deduce template arguments or pass explicitly?

Function templates allow writing a single definition that can handle multiple different types. It is a very powerful form of C++’s static polymorphism.

When instantiating a class template, we have to pass in the types explictly (at least until C++17):

std::vector<int> vec;
std::basic_string<my_char, std::char_traits<my_char>> str;
std::tuple<int, bool, std::string> tuple;

But when instantiating a function template, the compiler can often figure the types out:

template <typename A, typename B, typename C>
void func(const A& a, const B& b, const C& c);

int x;
func(x, 'A', "hello");
// equivalent to:
func<int, char, const char*>(x, 'A', "hello");

Let’s look at this process into a little bit more detail and establish some guidelines as well as see how we can prohibit template argument deduction for arguments.


Advertisement

Template argument deduction 101

This is just a quick overview, for more details check out Scott Meyer’s talk about it.

When the template arguments are deduced, there are two distinct types: The type of the template argument and the type of the parameter, they depend on the type of the argument. There are three cases for deduction, each behaving slightly differently, depending on how the parameter is declared.

1) T param or T* param

If you have a value or pointer parameter, the type of the template argument is the decayed type of the argument, e.g. without const/volatile or references:

template <typename T>
void func(T param);

int x;
const int cx = 0;

func(x); // argument is int&, T is int
func(cx); // argument is const int&, T is int

It strips every qualifier from the type because it is an entirely new variable, so there is no need to keep const-ness, for example. Depending on the qualifiers of the function parameter, the type of the function parameter is just the type of T with those qualifiers, but this does not change the type of T.

template <typename T>
void func(const T param); // type will be const T
template <typename T>
void func(T* param); // type will be pointer to T

Note that if you have a pointer, the argument type must be convertible to that pointer. Also note that only the top-level const/volatile of the pointer is stripped, if you have a pointer to const, this will stay:

template <typename T>
void func(T* param);

int* ptr;
const int* cptr;
int* const ptrc;
func(ptr); // argument is int*&, T is int, param is int*
func(cptr); // argument is const int*&, T is const int, param is const int*
func(ptrc); // argument is int* const&, T is int, param is int*

2) T& param

If you have a parameter that is an lvalue reference, it will only strip the reference from the type of the argument, but keep const/volatile, pointers etc. for the type of T:

template <typename T>
void func(T& param);

int x;
const int cx = 0;
int* ptr = &x;

func(x); // argument is int&, T is int
func(cx); // argument is const int&, T is const int
func(ptr); // argument is int*, T is int*

The parameter type will just be the type of T with the reference added again. If you have a const T& param, this will also ensure that the reference is a reference to const. If param is not a reference to const, the argument must be an lvalue. But note that you can make it a reference to const with a plain T& param:

template <typename T>
void func1(T& param);
template <typename T>
void func2(const T& param);

int a = 0;
const int b = 0;

func1(std::move(a)); // argument is int&&, T is int, param is int&, cannot bind
func2(std::move(a)); // argument is int&&, T is int, param is const int&, can bind

func1(std::move(b)); // argument is const int&&, T is const int, param is const int&, can bind
func2(std::move(b)); // argument is const int&&, T is const int, param is const int&, can bind

I literally learned that will writing this blog post: You can pass rvalue to functions taking T& - but only const rvalues.

3) T&& param

If you have parameter of the form T&&, where T is a direct template parameter of the function, it is not actually an rvalue reference but a forwarding reference. This does not happen for const T&& param or foo<T>&& param or in std::vector<T>::push_back(T&&), only for cases like shown above. Then the argument deduction rules are special, the type of T will be the exact same type as the argument (unless the argument is a plain rvalue in which case it will deduce like regular references, it’s weird):

template <typename T>
void func(T&& param);

int x;
const int cx = 0;
int* ptr = &x;

func(x); // argument is int&, T is int&
func(cx); // argument is const int&, T is const int&
func(ptr); // argument is int*&, T is int*&
func(0); // argument is int&&, T is int (param will be int&& anyway)

To paraphrase Scott Meyers: This is a hack special rule to allow perfect forwarding of arguments.

Because due to something called reference collapsing, the type of param will be the same as the type of T and thus the same as the type of the argument. With it you can perfectly forward arguments, but that is beyond the scope of this post, so let’s move on.

Template argument deduction is amazing

You’ve probably used function templates and template argument deduction long before you know these rules. This is because the rules “just work” - in most cases, they behave like expected and do exactly what you want.

Except for forwarding references, they’re weird. I’d rather have a special forwarding reference - T&&& or whatever - instead of this “special rule”.

So when calling a function template, there is no need to explictly pass the arguments, on the contrary, it can do harm! Consider the example I gave right at the start:

template <typename A, typename B, typename C>
void func(const A& a, const B& b, const C& c);

int x;
func(x, 'A', "hello");
// equivalent to:
func<int, char, const char*>(x, 'A', "hello");

We have reference parameters, so case two described above. This means that the type of the template argument will be the same as the type of the argument without references. The type of x is int&, so A will be int. The type of 'A' is char, so B will be char.

The type of a literal is not const, because reasons.

But what’s the type of "hello"? const char*?

Wrong.

The type of a string literal is an array, not a pointer.

This is not noticed usually because arrays really hate being arrays and decay to pointers whenever possible.

In particular, the type of "hello" is const char[6] - here we have a const, because of different reasons. const char[6] with references stripped is … const char[6] and not const char*, so actually the call would be equivalent to:

func<int, char, const char[6]>(true, "hello");

I did that mistake on purpose, to make my point clear: Template argument deduction is smarter than you and makes fewer errors.

In this case the example would not do harm. But consider a function that perfectly forwards arguments to a constructor

  • if you mess up the types, it might create unnecessary temporaries or do a copy instead of a move! Messing up the types can have runtime penalties.

This leads to the following guideline:

Guideline: Let the compiler deduce template arguments and don’t do it yourself

Manually deducing template arguments is a repetitive, boring, error-prone and - most importantly - unnecessary task. The compilers is way better than you in doing such stuff, so - to take the words of STL - don’t help the compiler.

So just don’t pass the template arguments explictly.

But: Template argument deduction isn’t perfect

But sometimes, you do not want template argument deduction.

To understand why, we need to take a closer look at the forwarding reference deduction case again:

Didn’t I say, that I won’t go into it in more detail? Well, apparently I lied.

template <typename T>
void other_func(T t);

template <typename T>
void func(T&& t)
{
    // perfectly forward t to other_func
}

A forwarding reference is used to forward stuff, e.g. here to other_func(). other_func() needs a copy of it’s argument, so we want to ensure that it will be moved when it is an rvalue and copied when it is an lvalue. Basically, it should behave like so:

other_func(val); // copy
func(val); // also copy

other_func(std::move(val)); // move
func(std::move(val)); // also move

A naive implementation of func() would look like this:

template <typename T>
void func(T&& t)
{
    other_func(t);
}

I’ve told you that t will be exactly the same as the argument, so an rvalue reference if the argument was an rvalue, and an lvalue reference if the argument was an lvalue.

But this does not mean that other_func(t) will move the argument if t is an rvalue reference. It will copy t, because in func() t has a name and can be assigned to - inside the function it is an lvalue!

So this implementation will always copy and never move.

We can’t write other_func(std::move(t)) either, because it will always move, even for lvalues!

What we need is a function that behaves like std::move() for rvalue and returns the argument unchanged for rvalues. This function has a name, it is called std::forward(). You could implement it like so, remember, like std::move(), all it needs is cast the argument:

template <typename T>
T&& forward(T&& x)
{
    return static_cast<T&&>(x);
}

If you pass an lvalue, T will be deduced to an lvalue reference, reference collapsing of lvalue reference and && make the function identical to:

template <typename T>
T& forward(T& x)
{
    return static_cast<T&>(x);
}

For rvalues the forwarding reference will behave like a regular reference in terms of deduction, so T will be the type of the arguments without the reference and the parameter will become a regular rvalue reference to T.

But this implementation has a flaw, we could use it in func() like so:

other_func(forward(t));

What’s the problem, you ask. We said that forward() will return an rvalue for rvalues (so move t), and an lvalue for lvalues (so copy t).

The problem is the same as before: in the function t is an lvalue, so it will always return an lvalue as well! In this case we actually cannot rely on template argument deduction, we have to specify the argument ourselves:

other_func(forward<T>(t));

Remember, for rvalues T is an rvalue reference, so it will force reference collapsing to handle an rvalue. While for lvalues T is an lvalue as well, so it returns an lvalue.

For that reason, std::forward() is implemented in a way that requires you to explictly specify the template argument, it has prohibited deduction.

Technique: Preventing template argument deduction

Sometimes you don’t want template argument deduction as it would lead to the wrong results. The most notable example is std::forward().

This can be achieved very easily, just put it in a non-deduced context:

template <class Container>
void func(typename Container::iterator iter);

std::vector<int> vec;
func(vec.begin());

In this call the compiler cannot deduce that the type of Container is std::vector<int>. It simply can’t do such advanced pattern matching. Whenever the template parameter is not used as parameter directly, but instead the parameter type is some member type or a template instantiated with the parameter or similar, it is in a non-deduced context and the caller has to pass the type explictly.

This can be used to prevent deduction of template arguments:

template <typename T>
struct identity
{
    using type = T;
};

template <typename T>
void func(typename identity<T>::type t);

While t will always have the type of T, the compiler does not know of any later specializations of identity and can’t assume that, so it can’t deduce the type.

This technique is also used in std::forward().

Amended Guideline: Let the compiler deduce template arguments unless it can’t

As we’ve seen, there are some cases where template argument deduction is not possible: It could have been prevented by the programmer, or template parameters that are not used in the parameters at all, like in std::make_unique():

template <typename T, typename ... Args>
std::unique_ptr<T> make_unique(Args&&... args);

Here T is only used in the return type, so it cannot be deduced at all and has to be passed in explictly. So in those cases: manually specify the template arguments and otherwise let the compiler do it for you.

This guideline doesn’t seem as nice as the first one. Previously, any call of the form func<T>(args) was a violation and could be flagged, now it has to be done on a case by case basis. Because there is no way to require deduction for certain types, every function has to document which template parameters are meant to be deduced and which are meant to be passed in explictly. This is unnecessary and can lead to silly mistakes, that are not detected.

So let’s try to enable template argument deduction for every parameter.

Technique: Tag templates

Consider yet another example where template argument deduction is not possible:

template <std::size_t I, class Tuple>
some-type get(Tuple&& t);

We have to pass the index to std::get as explicit template argument, it can’t be deduced from the arguments.

What we need is a tag template. Like a tag type it is a parameter of the function that is not really used and just there for technically reasons. In this case it is not a type, but a template, and should enable template argument deduction.

What we need is a way to make I part of the signature. For that we need a parameter to get() whose type depends on I - std::integral_constant, for example:

template <std::size_t I, class Tuple>
some-type get(std::integral_constant<std::size_t, I>, Tuple&& tuple);

Now, instead of calling get like so:

get<0>(tuple);

We call it like so:

get(std::integral_constant<std::size_t, 0>{}, tuple);

We pass an object of the instantiation of the tag template we want. Granted, like so, that’s verbose, but we can easily alias it:

template <std::size_t I>
using index = std::integral_constant<std::size_t, I>;

template <std::size_t I, class Tuple>
some-type get(index<I>, Tuple&& tuple);

get(index<0>{}, tuple);

We can even go one step further with something like Boost Hana’s UDLs:

get(0_c, tuple);
// _c is a user-defined literal
// it returns the integral_constant corresponding to the value

The same also works for types, just need to define a tag template that depends on some type:

template <typename T>
struct type {};

And use it like so:

template <typename T, typename ... Args>
T make(type<T>, Args&&... args);

auto obj = make(type<std::string>{}, "hello");

This can also be used with functions where we don’t want deduction:

template <typename T>
void non_deduced(type<T>, typename identity<T>::type x);

non_deduced(type<short>{}, 0);

The identity trick disables deduction for the actual argument, so that you won’t have conflicting types for the parameters.

The tag template is a lightweight parameter that just drives argument deduction, to ensure that everything can be deduced and our original guideline is valid in every case.


Advertisement

Conclusion

Phew, that post got long.

All I want to say is the following:

  • Don’t help the compiler, use template argument deduction. It does the job better than you ever could.

  • In the rare case where template argument deduction does screw up, disable it by putting the argument in a non-deduced context.

  • In cases where template argument deduction isn’t possible, consider using a tag template to enable deduction anyway.

The third point is controversial and definitely seems weird, but if used throughout the code bases it gives you consistency. Whenever you explictly pass template arguments, that’s a violation of the guideline.

But even if you don’t agree with my conclusion, I hope you’ve learned a thing or two related to template argument deduction.