Class Template Argument Deduction in C++17

Share the Article

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>

compiler output of CTAD - class template argument deduction basic example

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

compiler error on using multiple types in single vector

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>

Output of code using CTAD concept with user-defined class
i

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:

  1. Single vector variable doing initialization
  2. 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

demonstration of template argument deduction when multiple vectors are sent in argument list

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

example output of general callback function, implemented using template argument deduction

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

compiler error on partial CTAD

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

basic example using a deduction guide with CTAD

Decaying of char[array] argument to const char* type

An argument of type char[ ] can decay to const char * in 2 cases:

  1. When char[ ] argument is passed to function template which accepts by-value parameter, then it automatically decays to pointer
  2. 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*

decaying of char[13] to const char* on passing this array by value

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]

output of code which uses pass-by reference with class template argument deduction
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; }

Output

output of code using class template argument deduction with deduction guide

Main Funda : From C++17, there is no need to specify template type in angular brackets during instantiation of class

Related Topics:

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 *