Deducing Types
Contents
Introduction
This section examines how C++ figures out types. There are 3 general areas where this applies: "Template functions", "auto" and "decltype" . The "auto" and "decltype" are new features of C++ 11.A useful site for checking the type deduction is:
https://cppinsights.io/
We can paste our C++ code in the left hand side and the deduced type will appear in another window on the right hand side.
Template Function
Say we have: templatevoid f(ParamType param); When we call the function what is the type that gets assigned to "T" and "param" ? In some cases the type is different because of const, reference and pointer qualifiers.
Pass by Reference
File: auto1.cpp
#include <iostream> using namespace std ; //-------------------------------------------------------- template<typename T> void f1(T& param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //param++ ; } //-------------------------------------------------------- int main() { int x1 = 27; // x is an int const int cx = x1; // cx is a const int const int& rx = x1; //What is T amd paramtype. They can be different due to qualifiers //such as const, ref. // int and int& f1( x1 ) ; //T is an int param is an int& //We need to make sure that the constness is preserved //const int f1( cx ) ; //T is a const int param is an const int& f1( rx ) ; //T is a const int param is an const int& return 0 ; } //-------------------------------------------------------- Output: $ rm a.exe ; g++ auto1.cpp ; ./a.exe param:27 i i param:27 i i param:27 i i The above file shows an example of a template function taking a parameter that is a reference. templatevoid f1(T& param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //param++ ; } If we call the function with "x1" int x1 = 27; // x is an int f1( x1 ) ; Then T is interpreted as int and param is interpreted as int& . We could uncomment "//param++" and the change would be reflected in "x1" .
Reference
File: auto2.cpp
#include <iostream> using namespace std ; //-------------------------------------------------------- template<typename T> void f1(T& param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; param++ ; } //-------------------------------------------------------- int main() { int x1 = 27; // x is an int const int cx = x1; // cx is a const int const int& rx = x1; //What is T amd paramtype. They can be different due to qualifiers //such as const, ref. // int and int& f1( x1 ) ; //T is an int. param is an int& cout << x1 << endl ; //We need to make sure that the constness is preserved //const int // f1( cx ) ; //T is a const int. param is an const int& // f1( rx ) ; //T is a const int. param is an const int& return 0 ; } //-------------------------------------------------------- $ rm a.exe ; g++ auto2.cpp ; ./a.exe param:27 i i 28 Next we pass a constant int to our function: const int cx = x1; f1( cx ) ; now T is a constant int and the param is a constant int&. That makes sense because our reference in (T& param) should not be allowed to modify the original constant "cx". If we uncomment: //param++ ; Then we see a compiler error of the form: auto2.cpp:11:8: error: increment of read-only reference ‘param’ 11 | param++ ; And for the third function call: const int& rx = x1; f1( rx ) ; We have a constant reference "rx" and the "param" will point to it. The "T" is a const int and param is a const int& . Again we will not be able to uncomment //param++ ; As we cannot change the value of our reference ( pointing to another reference ) .
Pass by Value
If the template function is written as: templatevoid f1(T param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; param++ ; } Then the calling function argument is passed by value and the reference and constness are not considered.
File: auto3.cpp
#include <iostream> using namespace std ; //-------------------------------------------------------- template<typename T> void f1(T param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; param++ ; } //-------------------------------------------------------- int main() { int x1 = 27; // x is an int const int cx = x1; // cx is a const int const int& rx = x1; f1( x1 ) ; //T is an int. param is an int cout << x1 << endl ; f1( cx ) ; //T is an int. param is an int f1( rx ) ; //T is an int. param is an int return 0 ; } //-------------------------------------------------------- $ rm a.exe ; g++ auto3.cpp ; ./a.exe param:27 i i 27 param:27 i i param:27 i iWe can modify param inside the function and that does not change the value of x1 because param is a local variable to the function. In all 3 cases the T is int and param is also of type int.
We can also pass pointers by value.
File: ptr1.cpp
#include <iostream> using namespace std ; //-------------------------------------------------------- template<typename T> void f1(T param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; (*param)++ ; //Allowed } //-------------------------------------------------------- int main() { int x1 = 27; // x is an int //T is int* and param is int* int* ptr1 = &x1 ; f1( ptr1 ) ; cout << return 0 ; } //-------------------------------------------------------- Output: $ rm a.exe ; g++ ptr1.cpp ; ./a.exe param:0x7ffffcc24 Pi Pi 2 Both the T and param are of type int*. The constness is also preserved.
File: ptr2.cpp
#include <iostream> using namespace std ; //-------------------------------------------------------- template<typename T> void f1(T param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //(*param)++ ; Compiler error } //-------------------------------------------------------- int main() { int x1 = 27; // x is an int const int* ptr1 = &x1 ; //T is const int* and param is const int* f1( ptr1 ) ; return 0 ; } //--------------------------------------------------------
Next we consider the case where the function is of the form pass by reference to constant:
Pass by reference to constant
templatevoid f1(const T& param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //param++ ; }
File: auto4.cpp
#include <iostream> using namespace std ; //-------------------------------------------------------- //const reference template<typename T> void f1(const T& param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //param++ ; } //-------------------------------------------------------- int main() { int x1 = 27; // x is an int const int cx = x1; // cx is a const int const int& rx = x1; f1( x1 ) ; //T is an int. param is an const int& cout << x1 << endl ; f1( cx ) ; //T is an int. param is an const int& f1( rx ) ; //T is an int. param is an const int& return 0 ; } //-------------------------------------------------------- $ rm a.exe ; g++ auto4.cpp ; ./a.exe param:27 i i 27 param:27 i i param:27 i i Since the "const" is in the argument the T is of type int and the param is of type const int& in all the 3 cases.The pointer case is very similar.
Pass by pointer to constant
File: auto5.cpp
#include <iostream> using namespace std ; //-------------------------------------------------------- //const reference template<typename T> void f1(const T* param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //*param = 100 ; } //-------------------------------------------------------- int main() { int x1 = 27; // x is an int const int cx = x1; // cx is a const int const int& rx = x1; f1( &x1 ) ; //T is an int. param is an const int* cout << x1 << endl ; f1( &cx ) ; //T is an int. param is an const int* f1( &rx ) ; //T is an int. param is an const int* return 0 ; } //-------------------------------------------------------- $ rm a.exe ; g++ auto5.cpp ; ./a.exe param:0x7ffffcc24 i PKi 27 param:0x7ffffcc20 i PKi param:0x7ffffcc24 i PKi The function is defined as: templatevoid f1(const T* param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //*param = 100 ; } The argument "const T* param" means a pointer to a constant. In all 3 cases the T is of type int and param is of type const int* . Now we remove const in the template function so that our function looks like: template void f1(T* param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //*param = 100 ; } To preserve constness the const is moved to the type T .
Pass by pointer
File: auto6.cpp
#include <iostream> using namespace std ; //-------------------------------------------------------- //Normal pointer template<typename T> void f1(T* param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //*param = 100 ; } //-------------------------------------------------------- int main() { int x1 = 27; // x is an int const int cx = x1; // cx is a const int const int& rx = x1; f1( &x1 ) ; //T is an int. param is an int* cout << x1 << endl ; f1( &cx ) ; //T is a const int. param is a const int* f1( &rx ) ; //T is a const int. param is a const int* return 0 ; } //-------------------------------------------------------- Output: $ rm a.exe ; g++ auto6.cpp ; ./a.exe param:0x7ffffcc24 i Pi 27 param:0x7ffffcc20 i PKi param:0x7ffffcc24 i PKi f1( &x1 ) ; //T is an int. param is an int* f1( &cx ) ; //T is a const int. param is a const int* f1( &rx ) ; //T is a const int. param is a const int* We cannot modify the original value. If we uncomment //*param = 100 ; we receive an error of the form: auto6.cpp:30:7: required from here auto6.cpp:12:10: error: assignment of read-only location ‘* param’ 12 | *param = 100 ; The const int shifts to the type T as it is not defined in the parameter.
Pass by universal reference
Next we consider the special case of universal reference which is a special type of reference. The syntax is writtern as: templatevoid f1(T&& param) We know that once a variable is declared it is a lvalue. However in some cases we want the lvalue, rvalue property to be preserved when we pass it to a function such as "f1" above. The below program illustrates this behavior.
File: auto7.cpp
#include <iostream> using namespace std ; //-------------------------------------------------------- //Universal Reference template<typename T> void f1(T&& param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //*param = 100 ; } //-------------------------------------------------------- int main() { int x1 = 27; // x is an int const int cx = x1; // cx is a const int const int& rx = x1; f1( x1 ) ; //T is an int&. param is an int& //Lvalue reference cout << x1 << endl ; f1( cx ) ; //T is a const int&. param is a const int& //Lvalue reference f1( rx ) ; //T is a const int& param is a const int& //Lvalue reference f1( 23 ) ; //T is int param is a int&& // Rvalue reference return 0 ; } //-------------------------------------------------------- Output: $ rm a.exe ; g++ auto7.cpp ; ./a.exe param:27 i i 27 param:27 i i param:27 i i param:23 i i
Auto
The word "auto" is used for variable and the compiler figures out the type from what we assign to it. This can be useful in certain situations such as the code below:vectorThe rules for type deduction are very similar to the template type deduction.::iterator it;
templatevoid f1(T& param) { cout << "param:" << param << " " << typeid(T).name() << " " << typeid(param).name() << endl ; //param++ ; } auto& x1 ; For a template function we can place the qualifier in the parameter and the "typename T" gets the basic type. For "auto" we place the qualifier in the auto keyword and the variable gets the basic type.
File: auto8.cpp
#include <iostream> using namespace std ; //Using auto //-------------------------------------------------------- int main() { int x1 = 27; // x is an int const int cx = x1; // cx is a const int const int& rx = x1; int* ptr1 = &x1 ; const int* ptrc1 = &x1 ; //Can't do //*ptrc1 = 100 ; auto ax1 = x1 ; //ax1 is an int auto acx1 = cx ; //acx1 is an int auto arx1 = rx ; //arx1 is an int const auto acx2 = cx ; //acx2 is a const int const auto& arx2 = rx ; //arx2 is a const int& auto ptr2 = ptr1 ; //ptr2 is a int* auto ptrc2 = ptrc1 ; //ptrc2 is a int* to a constant //Universal reference auto&& ur1 = x1 ; //lvalue reference int& auto&& ur2 = 27 ; //rvalue reference int&& int j1 = 27 ; int j2(27) ; int j3 = { 27 } ; int j4{ 27 } ; auto i1 = 27; //int i1=27 auto i2(27); //int i2=27 auto i3 = { 27 }; // std::initializer_list<int> i3 = std::initializer_list<int>{27}; auto i4{ 27 }; //int i4 = {27}; return 0 ; } //-------------------------------------------------------- The above code shows some auto examples. The statement: auto acx1 = cx ; can be thought of as passing by value so "acx1" ends up being of type int. If we do want to retain the constness then we need to qualify the auto keyword. const auto acx2 Now "acx2" is of type const int. Note that auto ptr2 = ptr1 ; ptr2 gets the type int* assigned to it which is what we would expect. How about a pointer to a constant ? const int* ptrc1 = &x1 ; auto ptrc2 = ptrc1 ; ptrc2 gets the correct type of const int* //Universal reference auto&& ur1 = x1 ; //lvalue reference int& auto&& ur2 = 27 ; //rvalue reference int&& The above shows how to declare a universal reference with auto. int j1 = 27 ; int j2(27) ; int j3 = { 27 } ; int j4{ 27 } ;
Decltype
The keyword "decltype" is used to obtain the type from the name of a variable or an expression. We can then use that type to declare variables.File: decl1.cpp
#include <iostream> using namespace std ; class A1 { }; //-------------------------------------------------------- int main() { int x1 = 27; // x is an int const int cx = x1; // cx is a const int const int& rx = x1; int& lvalueRef = x1 ; int* ptr1 = &x1 ; const int* ptrc1 = &x1 ; decltype( x1 ) x2 =10 ; cout << typeid( x2 ).name() << endl ; //typeid does not give information about //constant qualifiers decltype( cx ) cx1 =10 ; //cx1 is of type const int cout << typeid( cx1 ).name() << endl ; decltype( rx ) rx1 =10 ; // rx1 is of type const int& if a reference is constant then // we can assign a rvalue to it cout << typeid( rx1 ).name() << endl ; decltype(lvalueRef) lvalueRef1 = x1 ; //need to initialize it here as lvalueRef1 is of type int& A1 Obj1A ; decltype(Obj1A) Obj2A ; //Obj2A is of type A1 auto rx2 = rx1 ; //auto does not retain the const //rx2 is of type int decltype( auto ) rx3 = rx1 ; //The above syntax is stating that //rx3 is of type const int& return 0 ; } //-------------------------------------------------------- In the above we have: int x1 = 27; decltype( x1 ) x2 =10 ; The "decltype(x)" comes as int. The decltype is slightly different from auto as it preserves the qualifiers of the type of the variable we are using in decltype argument. Similarly we have: const int& rx = x1 ; decltype( rx ) rx1 =10 ; and "rx1" is also of type "const int&" So what is the use case for decltype. This is usually used in certain scenarios. Let's say we have a template function such as: template// works, but auto function1(MyVector& c, Index i) // requires -> decltype(c[i]) The return type of this function varies but is not always "T". It's actually dependent on what type T is and the type of element returned by c[i] which may or may not be the same as type "T". It's decltype to the rescue, as we can specify that the return type be of type "c[i]" .
File: decl2.cpp
#include <iostream> #include <vector> using namespace std ; //-------------------------------------------------------- template<typename MyVector, typename Index> // works, but auto function1(MyVector& c1, Index i1) // requires -> decltype( c1[i1] ) { return c1[i1] ; } template<typename MyVector, typename Index> // works, but auto function2(MyVector& c1, Index i1) // requires { return c1[i1] ; } template<typename MyVector, typename Index> // works, but decltype(auto) function3(MyVector& c1, Index i1) // requires { return c1[i1] ; } //-------------------------------------------------------- int main() { vector<int> v1 ; v1.push_back( 100 ) ; v1.push_back( 101 ) ; cout << "function1( v1, 0 ) " << function1( v1, 0 ) << endl ; function1( v1, 0 ) = 101 ; //valid cout << "function2( v1, 0 ) " << function2( v1, 0 ) << endl ; //function2( v1, 0 ) = 101 ; //invalid //auto by itself takes out the reference cout << "function3( v1, 0 ) " << function3( v1, 0 ) << endl ; function3( v1, 0 ) = 101 ; //valid return 0 ; } //-------------------------------------------------------- $ rm a.exe ; g++ decl2.cpp ; ./a.exe rm: cannot remove 'a.exe': No such file or directory function1( v1, 0 ) 100 function2( v1, 0 ) 101 function3( v1, 0 ) 101 templateWe can also use the notation// works, but auto function1(MyVector& c1, Index i1) // requires -> decltype( c1[i1] ) { return c1[i1] ; } We are stating that the return type is of type "c1[i1]". Most of the STL containers will return a reference to the element if we use the array syntax with an index. However for certain containers such as "vector " the element is not of type bool. The decltype gives us the right type regardless of the MyVector type. With C++ 14; we can just use the word auto . template // works, but auto function2(MyVector& c1, Index i1) // requires { return c1[i1] ; } This introduces a problem. Since auto throws away the constant and the reference part we get the container element but we cannot assign to it. //function2( v1, 0 ) = 101 ; //invalid That's because the return value is not a reference. We have seen that to retain the reference we need to qualify the auto keyword. So we can do: template // works, but auto& function2a(MyVector& c1, Index i1) // requires { return c1[i1] ; } However this is a bit restrictive. A better way is to use the notation decltype(auto) . template // works, but decltype(auto) function3(MyVector& c1, Index i1) // requires { return c1[i1] ; } This is stating that use "auto" to deduce the type and apply the decltype rules.
Exercises
Fill in the code for the "adder" functions.
File: var_ex1.cpp
Solutions
File: var_soln1.cpp