Template type deduction in C++ — Behind the magic.

Ryonald Teofilo
7 min readAug 6, 2023

--

Template type deduction in C++

Template programming is a powerful built-in feature of C++ that most developers neglect to take the time to truly understand — mostly because it “just works”.

However, it is important to be cognizant of the template type deduction process, since picking the wrong deduction behaviour could cause unintended side effects — such as calling incorrect function overloads or casting to erroneous types. This may lead to performance being left on the table, or worse, undefined behaviour.

Although there are a few SO threads that do a decent job at explaining, most online tutorials on templates are brief and vague. Hence, I am writing this digestible guide to everything you know about template type deduction.

Getting familiar

template<class T>
void Func(T& t)
{
// do Func() things
}

int a = 0;
Func(a); // what is T deduced as here?

Taking a look at an example of a template function above, Func() takes a reference to a T. If we pass it a variable a of type int, T is deduced as int, with the function parameter type becoming int&.

Simple, right? At this point, most devs would turn around and move with their day — and understandably so. It may seem like T is simply the type of argument being passed in. However, this is not always the case.

template<class T>
void Func(T&& t)
{
// do Func() things
}

int a = 0;
Func(a); // now what is T?

Notice above, the parameter type for Func() has changed to T&&. In this instance, both T and the parameter type will be int&. Yes, deduced type is a indeed a reference!

Template function parameter types

The keen eye among us may have already observed that alongside T , the template function’s parameter type plays a major role in what type of deduction behaviour will be used.

Writing templates functions, we do not have control over the types of arguments that will be passed into our templates (if we are not expecting certain types, practice SFINAE!). Therefore, in order to get our compiler to deduce the right types, we are responsible for choosing the correct function parameter type.

Let me introduce a snippet of pseudocode to help get my point across.

template<class Type>
void Func(ParamType param);

Func(arg);

As illustrated above, Type, ParamType and arg represent the deduced type, template function parameter type and argument respectively.

We can split ParamType into three categories: ParamType of a pointer or reference, ParamType of neither a pointer nor reference, and ParamType of a universal or forwarding reference.

template <class T>
void Foo(T& t); // ParamType of a pointer or a reference

template <class T>
void Foo(T t); // ParamType of neither a pointer nor reference (copy)

template <class T>
void Foo(T&& t); // ParamType of a universal or forwarding reference

ParamType — a pointer or reference

template<class T>
void Func(T& param); // ParamType is a reference

As observed in our first example, we know that this is the most straightforward case. The type deduced for Type is essentially the type of arg.

For instance, if we call Func() with arg of type int. Type will be deduced as an int, while ParamType becomes an int&.

int a = 0;
Func(a); // Type is int, ParamType is int&

If we replace arg with a variable of type const int, Type will be deduced as a const int, and ParamType becomes a const int&.

const int ca = a;
Func(ca); // Type is const int, ParamType is const int&

And if we pass arg of an reference type, we get the same behaviour as previous examples — just with its reference-ness ignored. Type is deduced as a const int and ParamType becomes const int&.

const int& cra = ca;
Func(cra); // Type is const int, ParamType is const int&

Therefore, for template functions where ParamType is a pointer or reference, the following type deduction occurs.

ParamType — a pointer or reference

The pattern-matching is more noticeable when ParamType contains additional qualifiers. For instance, if we add a const qualifier to ParamType — making it a reference to a const, theconst-ness of the arg no longer needs to be a part of Type.

template<class T>
void Func(const T& param); // ParamType is a reference-to-const

int a = 0
Func(a); // Type is int, ParamType is const int&

const int ca = a;
Func(ca); // Type is int, ParamType is const int&

This process is similar if ParamType was a pointer, as seen below.

template<class T>
void Func(T* param);

int a = 0;
Func(&a) // Type is int, ParamType is int*

const int* pa = &a;
Func(pa) // Type is const int, ParamType is const int*

ParamType — not a pointer or a reference

Just like any function with a parameter that is neither a pointer nor reference, we are passing the argument by value. This means the argument passed into our function is copied, which reflects by the type of deduction that takes place.

template<class T>
void Func(T param); // ParamType is neither a pointer nor reference

int a = 0; // int
Func(a); // Type is int, ParamType is int

const int ca = a; // const int
Func(ca); // Type is int, ParamType is int

const int& cra = ca; // reference to const int
Func(cra); // Type is int, ParamType is int

As observed, any qualifiers and reference-ness of the argument are ignored. This is as expected because we are dealing with a new, independent copy of the original object that was passed as the argument.

For instance, passing in an argument of const int corresponds to Type and ParamType deduced as an int. Same goes for passing in an argument of const int&.

Thus, we can draw the behaviour for template function parameter type that is neither a pointer nor reference as the following.

ParamType — not a pointer or reference

For complete-ness, if a const pointer to a const type is passed in as an argument. The only thing being copied-by-value here is the pointer itself. This means that the const-ness of the object it is pointing to is preserved.

template<class T>
void Func(T param); // ParamType is neither a pointer nor reference

int a = 0;
const int* const cpa = &a;
Func(cpa); // Type is const int*, ParamType is const int*

ParamType — a universal or forwarding reference

This type is probably the most confusing, since the syntax for a universal (or officially known as a forwarding) reference template parameter is similar to a function that takes an rvalue reference (T&&). Because of this, the type of deduction that occurs might not be what most people expect.

template<class T>
void Func(T&& param); // ParamType is a universal reference

int a = 0;
Func(a); // Type is int&, ParamType is int&

Yes, you are reading that right, the deduced type is indeed a reference.

In order to understand this behaviour, I recommend focusing on the name itself — a “universal” reference. This should suggest its ability to receive an lvalue or rvalue and distinguish them. Let’s take a look at a few examples.

const int ca = a;
Func(ca); // Type is const int&, ParamType is const int&

const int& cra = ca;
Func(cra); // Type is const int&, ParamType is const int&

If we pass an lvalue const int, T and ParamType are deduced as const int&. This behaviour is similar when we pass it a reference.

So, if the argument is an lvalue, ParamType and T are deduced to be an lvalue reference.

However, if we pass it an rvalue int, ParamType is deduced as an int&& — an rvalue reference, and T is deduced as an int.

// Passing in an r-value
Func(42); // Type is int, ParamType is int&&

Hence, the process for template type deduction of a universal or forwarding reference parameter type could be drawn as such.

ParamType — universal or forwarding reference

It is important to note though, that this template function parameter type is the only one able to deduce to a reference type.

A common use of this is using templates to achieve perfect forwardingwhich in short is preserving the rvalue-ness of an argument using std::forward so the correct function overload will be called (if one that takes an r-value reference exists!).

template <class T>
class MyClass
{
public:
// c-tor with perfect forwarding
template<class U>
MyClass(U&& v): mVar(std::forward<U>(v)) {}

private:
T mVar;
};

int main()
{
int a = 7;
MyClass<int> myClass1(a); // Forwards lvalue to T's ctor
MyClass<int> myClass2(42) // Forwards rvalue to T's ctor
}

Template programming is powerful tool, but just like any powerful tool, the wielder has to know how to use it. For a C++ developer, that is going beyond understanding features as “just magic” :)

Feel free to leave a comment if you have any questions, happy coding!

Useful Resources:

Effective Modern C++ by Scott Meyers (O’Reilly)

Templates C++ reference

--

--