Type deduction

One of the most well-known features of C++11 is type deduction. This feature allows the compiler to infer the type of variables, functions, and expressions.

The two large features of type deduction introduced in C++11 and C++14 are auto and decltype.

In all examples, we will assume C++14, so don't forget to compile with the -std=c++14 flag.


auto

auto was introduced in C++11 to automatically deduce the types of variables based on their initialization.

A simple use can be seen here:

#include <iostream>

int main() {
  auto i = 42;
  std::cout << "i is " << 42 << std::endl;
  return 0;
}

Compiling this example and running the executable correctly outputs "i is 42". Excellent.

This is extremely useful when we have long, cumbersome types that we would prefer not to type. A classic example of this is the iterator type for containers in the C++ Standard Library:

std::vector<some_type>::iterator it = v.begin();

What a pain to write over and over. Because auto can deduce the type of it from the return type of std::vector::begin, we can simply write:

auto it = v.begin();

Consider the following example using auto for an iterator:

#include <iostream>
#include <vector>
#include <string>

int main() {
  std::vector<std::string> v;
  v.emplace_back("Hello");
  v.emplace_back("World");

  for(auto it = v.begin(); it != v.end(); ++it) {
    std::cout << *it << " ";
  }
  std::cout << std::endl;
  return 0;
}

A note about the above code: we use std::vector::emplace_back, which has a similar functionality to std::vector::push_back. Instead of pushing an instantiated object, std::vector::emplace_back will construct an object if the correct parameters to a constructor are passed. In this case, we have a std::vector<std::string>. In order to construct a std::string, we pass const char* to emplace_back and construct and "push" std::string into v.

One interesting feature of auto is that it doesn't capture reference automatically. This forces the programmer to decide whether or not the object is going to be changed. The following example illustrates this:

#include <iostream>
#include <vector>

struct A {
  A(int x) : x(x) {}
  int x;
};

int main() {
  std::vector<A> v;
  // Construct three A objects with x values of 1, 2, and 3 and "push" them into v.
  for(int i = 1; i <= 3; ++i) { 
    v.emplace_back(i);
  }

  // Here auto deduces the type of v[0] to be A, so a_by_value's type is A
  auto a_by_value = v[0];
  a_by_value.x = 100;
  std::cout << "By value: \n";
  for(auto it = v.begin(); it != v.end(); ++it) {
    std::cout << it->x << std::endl;
  }

  // Here auto deduces the type of v[0] to be A, but we qualify it with a reference, so a_by_ref's type is A&
  auto& a_by_ref = v[0];
  a_by_ref.x = 100;
  std::cout << "\nBy reference: \n";
  for(auto it = v.begin(); it != v.end(); ++it) {
    std::cout << it->x << std::endl;
  }

  return 0;
}

The above code produces the following output:

By value: 
1
2
3

By reference: 
100
2
3

This is what we expect, because in the "By value" section, we were changing a copy of v[0], but in the "By reference" section, we were changing a reference to v[0]. This is what we expected to see, so this is good.

Similarly, auto will not default to const-qualifying variables:

const int get_42() {
  return 42;
}

int main() {
  auto x = get_42(); // x is of type int, so it is mutable.
  const auto y = get_42(); // y is of type const int, so it is immutable.
  x++; // Legal
  // y++; // Illegal - y is read-only.
  return 0;
}

Since C++14, auto can also be used for function declarations and normally has a trailing return type:

#include <iostream>
#include <string>

auto get_greeting() -> std::string {
  std::string s("Hello, World!");
  return s;
}

int main() {
  auto s = get_greeting();
  std::cout << s << std::endl;
  return 0;
}

If a trailing return type is omitted, normal auto rules apply to the returned expression.

At first, this may seem silly. Why would we want to manually specify the return type of a function? Doesn't that defeat the purpose of using auto in a function signature?

To answer this question, we will begin our discussion of decltype.


decltype

decltype was introduced in C++11 to automatically yield the declared type of an expression.

decltype differs from auto in that it will qualify references and const if that is the declared type of an expression.

#include <iostream>

struct A {
  const int x = 42;
  int add_one_to_x() { return x + 1; }
};

int main() {
  A a;
  decltype(a.x) x = a.x;
  // x++; // Illegal - x is immutable because type of x is const int
  std::cout << "x is " << x << std::endl;
  decltype(a.add_one_to_x()) x_plus_one = a.add_one_to_x(); // x_plus_one is of type int
  std::cout << "x + 1 is " << x_plus_one << std::endl;
  ++x_plus_one; // Legal because x_plus_one is mutable                     
  std::cout << "x + 1 + 1 is " << x_plus_one << std::endl;

  return 0;
}

This outputs:

x is 42
x + 1 is 43
x + 1 + 1 is 44

This is a really powerful construct because now we don't need to know the return type of an expression to declare a variable of that type. Similarly, decltype can be used for the return type of a function. This is where auto is useful for functions.

Consider the trivial example where two objects are added:

#include <iostream>
#include <string>

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
  return t + u;
}

int main() {
  std::string s1("Hello, "), s2("World");
  auto x = add(1, 2.3);
  auto y = add(s1, s2);
  //  auto z = add(s1, 1.0); // Fails - std::string + double is not defined

  std::cout << x << std::endl;
  std::cout << y << std::endl;

  return 0;
}

This allows for the reuse of the function add for any two objects that support the operator +. Instead of overloading the function many times, we can simply use decltype. Additionally, the program will fail to compile if operator + is not defined on the two types, as seen in the section that is commented out.