This lesson is still being designed and assembled (Pre-Alpha version)

Type inference

Overview

Teaching: 10 min
Exercises: 0 min
Questions
  • Should I repeat again and again the obvious type of everything ?

Objectives
  • Know about auto and decltype.

Static typing and type inference

In C++, the developer must say the type of every piece of data which is handled, and the compiler will fiercely check that all your declarations are consistent. That is “static typing”.

Yet, there are more and more situations where you can ask the compiler to infer the types from the context. There are several benefits:

There is a drawback : when going above a threshold, which is rather subjective, you do not know any more which variable pertains to which type, and the code readability is damaged.

Old school type inference for function templates

When one call a function template, the compiler will consider the arguments given to the function, and can deduce the type parameters of the template. This is the first form of type inference introduced in C++. In the example below, the compiler will deduce that within main, for the call to sum, Num should deduced to be int.

#include <iostream>
#include <vector>

template< typename Num >
Num mean( const std::vector<Num> & col )
 {
  int sz = col.size() ;
  Num res = 0 ;
  for ( Num elem : col ) { res += elem ; }
  return res/sz ;
 }

int main()
 {
  std::vector<int> col = { 1, 2, 3, 4, 5 } ;
  std::cout << mean(col) << std::endl ;
 }
Get the full file: code/TypeInference/function-template-inference.cpp

Type inference for class templates

Type inference is now also available for classes. Below, notice how the compiler is able to deduce the type of col in main.

#include <iostream>
#include <vector>

template< typename Num >
Num mean( const std::vector<Num> & col )
 {
  int sz = col.size() ;
  Num res = 0 ;
  for ( Num elem : col ) { res += elem ; }
  return res/sz ;
 }

int main()
 {
  std::vector col = { 1, 2, 3, 4, 5 } ;
  std::cout << mean(col) << std::endl ;
 }
Get the full file: code/TypeInference/class-template-inference.cpp

New auto keyword, when initializing a variable

It is recommended to give an initial value to any variabe you declare. If the type of the variable is the same as the one of the initial value, do not repeat the type : use auto instead.

Rather than this:

#include <iostream>
#include <array>

int main()
 {
  std::array col = { 1, 2, 3, 4, 5 } ;

  int first = col[0] ;
  int sz = col.size() ; // bug ? unsigned to signed
  int sum = 0 ;
  for ( int elem : col )
   { sum += elem ; }

  std::cout << "first: " << first <<std::endl ;
  std::cout << "mean: " << sum/sz << std::endl ;
 }
Get the full file: code/TypeInference/noauto-variables.cpp

Write this:

#include <iostream>
#include <array>

int main()
 {
  std::array col = { 1, 2, 3, 4, 5 } ;

  auto first = col[0] ;
  auto sz = col.size() ; // bug ? unsigned to signed
  int sum = 0 ;
  for ( auto elem : col )
   { sum += elem ; }

  std::cout << "first: " << first <<std::endl ;
  std::cout << "mean: " << sum/sz << std::endl ;
 }
Get the full file: code/TypeInference/auto-variables.cpp

In the latter, no risk to make an error with the type of sz, and if you change your array from integers to doubles, no need to change the declaration of first or elem.

Yet, what to do with sum, which we want to initiate with the value 0 (of type int), but that should be of the same type as first (possibly double) ?

New decltype keyword

If you do not have a valid initial value for your new variable, you can rely on decltype to reuse the type of another variable.

#include <iostream>
#include <array>

int main()
 {
  std::array col = { 1, 2, 3, 4, 5 } ;

  auto first = col[0] ;
  auto sz = col.size() ; // bug ? unsigned to signed
  decltype(first) sum = 0 ;
  for ( auto elem : col )
   { sum += elem ; }

  std::cout << "first: " << first <<std::endl ;
  std::cout << "mean: " << sum/sz << std::endl ;
 }
Get the full file: code/TypeInference/decltype-variables.cpp

Keyword auto remove any const or &

Be aware that auto gets rid of any const or & modifier to the type of the original value, which is what you expect in most of the cases. In the example below, we obviously do not want res to be of type const int &.

#include <iostream>
#include <vector>

int accumulate( const std::vector<int> & col, const int & init )
 {
  auto res = init ;
  for ( auto elem : col )
   { res += elem ; }
  return res ;
 }

int main()
 {
  std::vector col = { 1, 2, 3, 4, 5 } ;
  std::cout << accumulate(col,0) << std::endl ;
 }
Get the full file: code/TypeInference/remove-modifiers.cpp

If you want to preserve the possible const and/or & from the initial value, use decltype instead of auto.

Willingly adding const and/or &

On the contrary, you must sometimes add const and/or & to a deduced type which lacks those modifiers. For example, if you want to modify the elements of some collection. In the example below, notice the auto & elem.

#include <iostream>
#include <vector>

void scale( std::vector<int> & col, int factor )
 {
  for ( auto & elem : col )
   { elem *= factor ; }
 }

int main()
 {
  std::vector col = { 1, 2, 3, 4, 5 } ;
  scale(col,2) ;
  auto first = col[0] ;
  std::cout << first << std::endl ;
 }
Get the full file: code/TypeInference/add-modifiers.cpp

Keyword auto as a return type

The compiler can also deduce the return type of a function from the return instructions within the body of the function. This also works with several return instructions, provided they all return a value of the same type.

#include <iostream>
#include <vector>

auto accumulate( const std::vector<int> & col, int init )
 {
  auto res = init ;
  for ( auto elem : col )
   { res += elem ; }
  return res ;
 }

int main()
 {
  std::vector col = { 1, 2, 3, 4, 5 } ;
  std::cout << accumulate(col,0) << std::endl ;
 }
Get the full file: code/TypeInference/auto-return.cpp

This only works if the declaration and the definition (body) of the function are in the same file (i.e. inline or template functions).

Soon in C++20 : auto for function arguments

Logically enough, we should be able, soon, to use auto for function arguments…. Actually, this is hiding templates, and as above, it only works for functions which have their declaration and description in the same file.

#include <iostream>
#include <array>

auto accumulate( const auto & col, auto init )
 {
  auto res = init ;
  for ( auto elem : col )
   { res += elem ; }
  return res ;
 }

int main()
 {
  std::array col = { 1, 2, 3, 4, 5 } ;
  std::cout << accumulate(col,0) << std::endl ;
 }
Get the full file: code/TypeInference/auto-arguments.cpp

If you look at the final implementation of accumulate above, you will probably agree that too much auto makes the code probably more generic, but also less readable.

Key Points

  • The new keyword auto avoid you the error-prone typing of complex long type names.

  • A collateral benefit is that the code is more generic.

  • Yet, do not overuse auto, because the types contribute to the readability of the code.