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:
- Not a reference type, but, a parameter-by-value (T)
- A reference type (T&)
- 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
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
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
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
- Template parameter T is pass-by-value
- 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
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
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,
- The overall type of x (which is an array)
- The type T (which becomes an int)
- 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;
}