Template type deduction in functions

Share the Article

When we call a template function, and provide the template arguments in angular brackets “< >”, then we are instantiating it by explicitly specifying template type. However, it is possible to instantiate the templates without specifying template type. In such case, the C++ compiler can itself performs template type deduction by looking at the argument list.

template<typename T> void funda(T x) //template function, T is parameter { } int main() { funda<double>(2.1); //explicit <double> specification funda(4); //type deduction to int return 0; }

Type deduction scenarios

There are mainly 3 scenarios where the compiler applies the rules of template type deduction.

These scenarios are – when the Template Parameter T is:

  1. Not a reference type, but, a parameter-by-value (T)
  2. A reference type (T&)
  3. a universal reference type (T&&)

1. Template Parameter is not a reference (pass-by-value)

template<typename T> void funda(T x);

In such case, whatever the calling code provides, the compiler creates a new copy. This means, if the calling code provides, a reference variable, then compiler shall create a new copy of that variable. Therefore, the template parameter T will no longer be the reference variable.

Similarly, if the calling code provides, a const variable, then also T shall be a copy and it shall not be const.

The following example shows that in multiple scenarios, both the const-ness and reference-ness is not considered in deciding the template parameter type T.

#include <iostream> //main header using namespace std;//for namespace template<typename T> void funda(T x) //Template parameter T is pass-by-value { if(std::is_const_v<typename remove_reference<T>::type>) cout << "const "; if(typeid(x) == typeid(int)) cout << "int"; if(std::is_reference<decltype(x)>( )) if(std::is_lvalue_reference<decltype(x)>( )) cout << "&"; else cout << "&&"; cout << endl; } int main() { int x; funda(x); //T becomes int funda(4); //T becomes int int &y = x; int &&z = 3; funda(y); //T becomes int, & ignored funda(z); //T becomes int, && ignored const int m = 4; funda(m); //T becomes int, const ignored const int &n = m; funda(n); //T becomes int, const and & ignored return 0; }

Output

example output of code using template type deduction with function parameter as pass by value

2. Template parameter is a reference type

template<typename T> void funda(T& x);

In such case, the type of parameter T is deducted as exact type of argument variable sent by calling code. However, if such variable is already a reference variable, then the compiler simply ignores the reference-part to deduce type. But ultimately, due to “&” in parameter, the T finally becomes an L-value reference. In current case, L-value-ness of T causes one limitation that this function can not accept an R-value argument. However, this limitation shall go away if the signature is having “const” keyword.

template<typename T> void funda(const T& x); //accepts R-value also

Please note that in the following code, the “const” aruments deduces T to become “const”.

#include <iostream> //main header using namespace std;//for namespace template<typename T> void funda(T& x) { if(std::is_const_v<typename remove_reference<T>::type>) cout << "const "; if(typeid(x) == typeid(int)) cout << "int"; if(std::is_reference<decltype(x)>( )) if(std::is_lvalue_reference<decltype(x)>( )) cout << "&"; else cout << "&&"; cout << endl; } int main() { int x; funda(x); //T shall become int& // funda(4); //error as T is always L-value int &y = x; int &&z = 3; funda(y); //T shall become int& funda(z); //T shall become int& const int m = 4; funda(m); //T shall become "const int&" const int &n = m; funda(n); //T shall become "const inst&" return 0; }

Output

example output of code using template type deduction with function parameter as pass by reference

3. Template parameter is universal reference

template<typename T> void funda(T&& x);

In this case, the behavior shall be almost same as in case 2 (above). Therefore, T shall always become a reference type. The only difference here is that due to the specification of T as universal reference, the function now accepts both R-value and L-value arguments. Due to reference collapsing rules, if the calling code sends an R-value argument, then T deduces to become R-value reference and with L-value it becomes an L-value reference.

#include <iostream> //main header using namespace std;//for namespace template<typename T> void funda(T&& x) { if(std::is_const_v<typename remove_reference<T>::type>) cout << "const "; if(typeid(x) == typeid(int)) cout << "Type = int"; if(std::is_reference<decltype(x)>( )) if(std::is_lvalue_reference<decltype(x)>( )) cout << "&"; else cout << "&&"; cout << endl; } int main() { int x; funda(x); //T shall become int& funda(4); //T shall become int&& int &y = x; int &&z = 3; funda(y); //T shall become int& funda(z); //T shall become int& const int m = 4; funda(m); //T shall become const int& const int &n = m; funda(n); //T shall become const int& return 0; }

Output

example output of code using template type deduction with function parameter as universal reference

Passing an array to template function

Passing an array to function is a very special case. This is because in some cases, the array variable type can decay into pointer type. The pointer shall point to first element of the array. However, in case of pointers, 2 cases arise

  1. Template parameter T is pass-by-value
  2. Template parameter T is pass-by-reference

In former case, the array shall decay into respective pointer, but in latter case, it retains the array type.

Pass-by-value

#include <iostream> //main header using namespace std;//for namespace template<typename T> void funda(T x) { if(typeid(x) == typeid(int)) cout << "Type = int"; if(typeid(x) == typeid(int[4])) cout << "Type = int[4]"; if(typeid(x) == typeid(int*)) cout << "Type = int*"; if(std::is_reference<decltype(x)>( )) if(std::is_lvalue_reference<decltype(x)>( )) cout << "&"; else cout << "&&"; cout << endl; } int main() { int a1[4] = {1, 2, 3, 4}; funda(a1); return 0; }

Output

output of code which is passing an array to a function template, and array getting decayed to pointer

Pass-by-reference

#include <iostream> //main header using namespace std;//for namespace template<typename T> void funda(T& x) { if(typeid(x) == typeid(int)) cout << "Type = int"; if(typeid(x) == typeid(int[4])) cout << "Type = int[4]"; if(typeid(x) == typeid(int*)) cout << "Type = int*"; if(std::is_reference<decltype(x)>( )) if(std::is_lvalue_reference<decltype(x)>( )) cout << "&"; else cout << "&&"; cout << endl; } int main() { int a1[4] = {1, 2, 3, 4}; funda(a1); return 0; }

Output

output of code passing an array to function template

Getting size of an array using templates type deduction

As in above illustration, an array argument retains the actual array type. Therefore, one application is that we can create a sizeof function. In this case, we may break-down the parameter T further into element-type and array-size components.

template<typename T, std::size_t N> void sizeof_funda(T (&x)[N] )

The following program, prints 3 things,

  1. The overall type of x (which is an array)
  2. The type T (which becomes an int)
  3. Size of the overall array
#include <iostream> //main header using namespace std;//for namespace template<typename T, std::size_t N> void sizeof_funda(T (&x)[N] ) { //Print final type of parameter "x" if(typeid(x) == typeid(int)) cout << "Type(x) = int"; if(typeid(x) == typeid(int[4])) cout << "Type(x) = int[4]"; if(typeid(x) == typeid(int[5])) cout << "Type(x) = int[5]"; if(typeid(x) == typeid(int*)) cout << "Type(x) = int*"; if(std::is_reference<decltype(x)>( )) if(std::is_lvalue_reference<decltype(x)>( )) cout << "&"; else cout << "&&"; cout << ", "; //Printing type of parameter type T if(typeid(T) == typeid(int)) cout << "Type(T) = int"; if(typeid(T) == typeid(int[4])) cout << "Type(T) = int[4]"; if(typeid(T) == typeid(int[5])) cout << "Type(T) = int[5]"; if(typeid(T) == typeid(int*)) cout << "Type(T) = int*"; cout << ", Size of Array = " << N; cout << endl; } int main() { int a1[4] = {1, 2, 3, 4}; sizeof_funda(a1); int a2[5] = {1, 2, 3, 4, 5}; sizeof_funda(a2); return 0; }

Output

example code which implement sizeof method for an array of any type

Main Funda: C++ can deduce the template type from function arguments. There is no need to explicitly provide such type.

Related Topics:

Class Template Argument Deduction in C++17
What is a Tuple, a Pairs and a Tie in C++
C++ Multithreading: Understanding Threads
What is Copy Elision, RVO & NRVO?
Lambda in C++11
Lambda in C++17
What are the drawbacks of using enum ?
Which member functions are generated by compiler in class?
How to stop compiler from generating special member functions?
Compiler Generated Destructor is always non-virtual
How to make a class object un-copyable?
Why virtual functions should not be called in constructor & destructor ?
How std::forward( ) works?
Rule of Three
How std::move() function works?
What is reference collapsing?
How delete keyword can be used to filter polymorphism
emplace_back vs push_back

Share the Article

Leave a Reply

Your email address will not be published. Required fields are marked *