Home C++ Introduction Decisions Loops Input/Output Functions Stack and Heap References Arrays Searching and Sorting Recursion Pointers Character and Strings Structures Classes Inheritance Exceptions Templatess STL Modern C++ Misc Books ----

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:

template
void 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.


template
void 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:

template
void 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 i
We 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

template
void 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:

template
void 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:


template
void 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:
 vector::iterator it;
The rules for type deduction are very similar to the template type deduction.
template
void 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

template    // 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.

We can also use the notation

Exercises



Fill in the code for the "adder" functions.
File: var_ex1.cpp

Solutions




File: var_soln1.cpp