Before C++17, it was mandatory to provide template type in angle brackets (< >), when initializing a template class. This was the case even though the template type was evident from given arguments on RHS. However, from C++17, this rule is relaxed. Now, there is concept of Class Template Argument Deduction.
//Explicit type specification (before C++17)
std::vector<int> v1 = {2, 6, 9};
std::vector<double> v2 = {2.3, 6.1, 9.0};
Therefore, the compiler shall automatically deduce the template type from the given arguments.
Example, in following code, the compiler shall automatically understand that v1 is vector<int> and v2 is vector<double>.
//No type specification
std::vector v1 = {2, 6, 9};
std::vector v2 = {2.3, 6.1, 9.0};
Basic Examples of class template argument deduction
Following examples shows multiple syntax of vector declaration without providing any angle brackets.
#include <iostream> //main header
#include <vector> //for vector
using namespace std;//for namespace
int main()
{
//Examples where type is vector<int>
std::vector<int> v1; //explicit specification of <int>
std::vector v2 = {2,6,9}; //deduction with multiple args
std::vector v3 = {2}; //deduction with single arg
std::vector v4 {3}; //deduction without equal (=)
std::vector v5 {4,5,6}; // same
//Examples where type is vector<double>
std::vector v6 ={5.2, 6.8, 9.1, 1.1};//with equal (=)
std::vector v7 ={4.5}; //same
std::vector v8 {1.3}; //without equal (=)
std::vector v9 {1.4, 5.7, 6.0}; //same
cout << typeid(decltype(v1)).name() << endl;
cout << typeid(decltype(v2)).name() << endl;
cout << typeid(decltype(v3)).name() << endl;
cout << typeid(decltype(v4)).name() << endl;
cout << typeid(decltype(v5)).name() << endl;
cout << endl;
cout << typeid(decltype(v6)).name() << endl;
cout << typeid(decltype(v7)).name() << endl;
cout << typeid(decltype(v8)).name() << endl;
cout << typeid(decltype(v9)).name() << endl;
return 0;
}
Output
First 5 entries specify vector<int>
Other 4 entries specify vector<double>
Error scenario : class template argument deduction
To enable compiler perform type deduction, the arguments must satisfy the specification of constructor. In the following example, the vector v2 do not have homogeneous elements in argument list. Therefore, it cannot qualify to be a vector. The code therefore, shall throw compile time error.
#include <iostream> //main header
#include <vector> //for vector
using namespace std;//for namespace
int main()
{
std::vector<int> v1;
std::vector v2 = {2, 6, 9.1}; //Wrong, both int & double
return 0;
}
Output
Automatic Deduction in User-defined Class
The concept works not only with standard classes but also with User-defined classes. The following example shows initialization of class twins with 3 different syntax. In all 3 cases, the compiler can deduce the argument type automatically.
#include <iostream> //main header
using namespace std; //for namespace
template<typename T>
class twins
{
T num1;
T num2;
public:
twins(T n1, T n2)
{
num1 = n1;
num2 = n2;
}
};
int main()
{
twins t1 (1, 2); //Deduction with syntax#1
twins t2 {1, 2}; //Deduction with syntax#2
twins t3 = {1, 2};//Deduction with syntax#3
cout << typeid(decltype(t1)).name() << endl;
cout << typeid(decltype(t2)).name() << endl;
cout << typeid(decltype(t3)).name() << endl;
return 0;
}
Output
In all 3 cases, the type deduction is twins<int>
Special case – vectors as arguments
Till now, all the examples were having R-values integer list in curly brackets (initializer_list< > ). This matches the vector constructor and it creates a vector. However, things will be different when we have another vector doing initialization.
There are 2 cases:
- Single vector variable doing initialization
- multiple vectors doing initialization
In first case, the call will match a copy constructor and therefore, the deduction of type is performed as doing a copy operation. The deduction shall therefore, provide exactly same type, i.e., vector<int>
std::vector<int> v1;
std::vector v3{v1}; //Copy operation with deduction
However, when multiple vectors occur in list, then it is not a copy construction. The type deduction shall happen on the basis of elements provided in initializer_list. And since, the elements are a list of vectors.
std::vector<int> v1;
std::vector v3{v1, v1, v1, v1}; //no copy operation
Therefore, the output vector v3 in this case shall deduce to a new type vector of vectors, vector< vector<int> >
Following example demonstrates the behavior.
#include <iostream> //main header
#include <vector> //for vector
using namespace std; //for namespace
int main()
{
std::vector<int> v1;
std::vector v2 = {2,6,9};//vector<int>
std::vector v3{v1}; //vector<int>
std::vector v4 = {v1}; //vector<int>
std::vector v5(v1); //vector<int>
std::vector v6{v1, v1}; //vector<vector<int>>
cout << typeid(decltype(v1)).name() << endl;
cout << typeid(decltype(v2)).name() << endl;
cout << typeid(decltype(v3)).name() << endl;
cout << typeid(decltype(v4)).name() << endl;
cout << typeid(decltype(v5)).name() << endl;
cout << endl;
cout << typeid(decltype(v6)).name() << endl;
return 0;
}
Output
Ambiguous Case with vectors as arguments
In previous example, we saw that when there are multiple vectors on RHS, then it leads to deduction of new type. However, things can become ambiguous even with single argument. This can happen when the program provides arguments list using Variable Arguments method.
In below code, the arguments are sent to function funda(Args& …) having multiple arguments.
#include <iostream> //main header
#include <vector> //for vector
using namespace std;//for namespace
template<typename... Args>
void funda(const Args&... arg) //Variable Arguments
{
auto v = std::vector{arg...};
cout << typeid(decltype(v)).name() << endl;
}
int main()
{
std::vector<int> v1;
std::vector v2 = {2,6,9,81};
std::vector v3{v2};
funda(1, 2, 3); //deduces v to vector<int>
funda(1); //deduces v to vector<int>
funda(v2); //Ambiguous !!
funda(v2, v3); //deduces v to vector<vector<int>>
return 0;
}
Why the statement funda(v2) is ambiguous?
Because, there are 2 ways how the compiler can interpret the given argument in variable argument list. The 2 ways are as follows:
1. Interpret as single vector passed => deduces to vector<int>
2. Interpret as a list of vectors with single element => deduces to vector<vector<int>>
Currently, there is no standard rule in this case, therefore, every compiler on its own, interprets according to one of the options .
Application of Template Automatic Deduction
Using the concept, it is possible to implement a generic callback function. Such a generic callback can work with any function with any signatures. In other words, it can accept any number and any type of function arguments and it can return any kind of value.
Generic Callback Function
#include <iostream> //main header
using namespace std;//for namespace
template<typename T>
class GenericCallBack
{
T callbackfunction;
public:
GenericCallBack(T c) : callbackfunction(c) {}
template<typename... Args>
auto operator() (Args&&... args)
{
return callbackfunction(std::forward<Args>(args)...);
}
};
void funda1(int x) //First Callback
{
cout << "This is funda1("
<< x
<< ")"
<< endl;
}
auto mylambda = [](int x, int y) //Second Callback
{
cout << "This is mylambda("
<< x << ", "
<< y << ")"
<< endl;
return (x+y);
};
int main()
{
GenericCallBack g1 = funda1; //type void(int)
GenericCallBack g2 = mylambda; //type int(int, int)
g1(7);
int ret = g2(3,9);
cout << "Return = " << ret << endl;
return 0;
}
Output
Class Vs Function Template – Partial Deduction
For Partial template type deduction, the behavior of class and function template has differentiation. A class do not support partial deduction, whereas the function allows this.
In following example, both class and function template has 2 typenames. However, the default value of typename for second type is first typename.
template<typename T1, typename T2 = T1>
This means when the instantiating code do not provides explicit typename for the second typename, then only second type is deduced by argument type.
Unfortunately, the class template do not work like this. The following code shall not work
MainFunda<int> m5 = {3, 5.6}; //ERROR,
//first type is int by specification
//second type will also be int, using
//T2=T1 by default specification
The compiler can either do full deduction of both types, without explicit specification of any type.
Or compiler can just use the first type to decide the second type, by using the default type expression in this case.
Please note, that a function template do not have any problem for partial deduction. Therefore, following code shall work.
funda1<int>(3, 5.6); //WORKS with partial deduction
//first type becomes int by specification
//second type becomes double by deduction
Working Example
#include <iostream> //main header
using namespace std; //for namespace
//Class Template (Note T2 = T1)
template<typename T1, typename T2 = T1>
class MainFunda
{
public:
MainFunda(T1 t1=T1{}, T2 t2=T2{})
{
cout << "MainFunda Class Type = " ;
cout << typeid(decltype(t1)).name() << ", ";
cout << typeid(decltype(t2)).name() << endl;
}
};
//Function Template (note T2 = T1)
template<typename T1, typename T2 = T1>
void funda1(T1 t1=T1{}, T2 t2=T2{})
{
cout << "funda1 function Type = " ;
cout << typeid(decltype(t1)).name() << ", ";
cout << typeid(decltype(t2)).name() << endl;
}
int main()
{
//Template with Partial explicit specification
MainFunda<int> m5 = {3, 5.6}; //ERROR
funda1<int>(3, 5.6); //WORKS
return 0;
}
Output
Deduction Guide with class template argument deduction
When the compiler does class template argument deduction for a class, it primarily looks at the given argument list. However, it is possible to give instruction to compiler to deduce specific or all types in specific way.
Example, if MainFunda<T> is template class, then program can give a deduction guide in statement as shown below. This cause deduction to double even if double type is not passed.
template<typename T>
MainFunda(T&& t) -> MainFunda<double>;
In following example, the calling code shall pass first an int value and second a double value. However, due to deduction guide in both cases, the class template shall take double type.
#include <iostream> //main header
using namespace std; //for namespace
template<typename T>
class MainFunda
{
public:
MainFunda(T&& t)
{
cout << "MainFunda Class Type = " ;
cout << typeid(decltype(t)).name() << ", ";
cout << endl;
}
};
template<typename T>
MainFunda(T&& t) -> MainFunda<double>; // Deduction Guide
int main()
{
MainFunda m1 = {3}; //deduces to double
MainFunda m2 = {5.6}; //deduces to double
return 0;
}
Output
Decaying of char[array] argument to const char* type
An argument of type char[ ] can decay to const char * in 2 cases:
- When char[ ] argument is passed to function template which accepts by-value parameter, then it automatically decays to pointer
- When char[ ] argument is passed to function template by-reference parameter, then it decays only on using deduction guide
Case 1 : The following program demonstrates, pass by-value
#include <iostream> //main header
using namespace std; //for namespace
template<typename T>
class MainFunda
{
public:
MainFunda(T t) //Parameter by Value
{
cout << "MainFunda Class Type = " ;
cout << typeid(decltype(t)).name() << ", ";
cout << endl;
}
};
int main()
{
MainFunda m1 = {"main funda!!"}; //Deduces to const char*
return 0;
}
Output
Parameter type is const char*
Case 2 : The following program demonstrates, pass by-reference
When the code do not have deduction guide statement
#include <iostream>
using namespace std;
template<typename T>
class MainFunda
{
public:
MainFunda(const T& t) //Parameter by-reference
{
cout << "MainFunda Class Type = " ;
cout << typeid(decltype(t)).name() << ", ";
cout << endl;
}
};
int main()
{
MainFunda m1 = {"main funda!!"};
//deduces to char[13]
return 0;
}
Output
Parameter is char[13]
Same Example With Deduction Guide
#include <iostream> //main header
using namespace std; //for namespace
template<typename T>
class MainFunda
{
public:
MainFunda(const T& t) //by-reference
{
cout << "MainFunda Class Type = " ;
cout << typeid(decltype(t)).name() << ", ";
cout << endl;
}
};
template<typename T>
MainFunda(T) -> MainFunda<T>; //Deduction Guide
int main()
{
MainFunda m1 = {"main funda!!"};
//Deduces to const char*
return 0;
}