cpp

Introduction

The auto type specifier requests the compiler to deduce the type using the initializing expression. The compiler puts the deduced type in the place of auto. This type specifier can be used in the type definition of:

The auto type specifier allows us to write generic code, because we no longer have to put a specific type, but can ask the compiler to deduce it.

Motivation

Writing types in legacy C++ was cumbersome, arduous and inviting errors that a compiler sometimes was unable to catch. Typically, to iterate over a container of containers, we had to spell out the iterator type. Now it’s easy to declare an iterator by defining its type using the auto specifier. Here’s an example:

#include <deque>
#include <iostream>
#include <vector>

int
main()
{
  std::deque<std::vector<int>> d;

  // We iterate using iterators with an explicitely declared type.
  for(std::deque<std::vector<int>>::iterator i = d.begin();
      i != d.end(); ++i)
    for(std::vector<int>::iterator j = i->begin();
        j != i->end(); ++j);

  // We iterate using iterators, but let the compiler deduce the type.
  for(auto i = d.begin(); i != d.end(); ++i)
    for(auto j = i->begin(); j != i->end(); ++j);
}

Likewise, for a container of type T we can use the size function that returns a value of type T::size_t, but it’s easier to use auto:

#include <iostream>
#include <vector>

using namespace std;

int
main()
{
  vector<int> v = {1, 2, 3};
  auto size = v.size();

  // We can iterate backward with an index.
  for(auto i = v.size(); i--;)
    cout << v[i] << endl;

  // We can iterate forward with an index.  We can ask the comiler to
  // deduce the type from 0, but we cannot be sure it will be the same
  // as vector<int>::size_type.
  for(auto i = 0; i < v.size(); ++i)
    cout << v[i] << endl;

  // This is correct, but without type deduction.
  for(vector<int>::size_type i = 0; i < v.size(); ++i)
    cout << v[i] << endl;

  // We ask the compiler to take the type of an expression without
  // deduction.  This is correct, and the most general, but somehow I
  // don't like it.
  for(decltype(v.size()) i = 0; i < v.size(); ++i)
    cout << v[i] << endl;
}

Sometimes we are unable to put a type, because we do not know it, as for a closure, i.e., a functor of an anonymous type, that is the result of a lambda expression.

int
main()
{
  auto c = []{};
}

So far so good, because in the type definition we used auto only, but the definition can also include the type qualifiers and declarators.

Deduction of a variable type

Deduction of the auto type is the same as the deduction of the a template argument of the type kind.

The initialization of a variable looks like this:

type name = expression;

Type type of variable name can include qualifiers (const, volatile). Additionally, type can include the reference declarator & and the pointer declarator *. We are interested in the case, where the variable type includes the auto specifier. For instance:

const auto &t = 1;

A compiler treats such a variable initialization as the initialization of a function parameter in a function template, where:

A compiler has to deduce the argument of such an imaginary template (imaginary, because it’s not in the code, we just imagine it) and substitute auto with it.

Examples

The following examples should not be hard to understand, because we already know the deduction rules. To make sure that in the examples we think (deduce) right, we can use the following trick. A compiler is going to report an error with the type deduced.

template <typename T>
class ER;

int
main()
{
  // auto = int
  auto x = 1;

  // Uncomment the line to see the type of x.
  // ER<decltype(x)> er;
}

Here’s the variadic version:

template <typename... T>
class ER;

template <typename... T>
void foo(T... t)
{
  ER<T...> er;
}

void goo(auto... t)
{
  ER<decltype(t)...> er;
}

int
main()
{
  // Uncomment the lines to see the types reported.
  // foo(1, .1);
  // goo(1, .1);
}

A reference or pointer type

We can declare a reference to the data of a type that a compiler has to deduce. The data can be some other variable, a function or an array. Here’s an example:

void
foo()
{
}

int
main()
{
  const volatile int x = 1;

  // A reference to a variable.
  // auto = const volatile int
  auto &r1 = x;
  // auto = volatile int
  const auto &r2 = x;
  // auto = const int
  volatile auto &r3 = x;
  // auto = int
  const volatile auto &r4 = x;

  // A reference to a function.
  // auto = void()
  auto &f1 = foo;
  // The above is equivalent to this.
  using ft = void();
  ft &f2 = foo;

  // A reference to a C-style table.
  int t[] = {1, 2, 3};
  // auto = int[3]
  auto &t1 = t;
  // The above is equivalent to this.
  using t3i = int[3];
  t3i &t2 = t;
}

Likewise for pointers:

void foo()
{
}

int
main()
{
  const volatile int x = 1;

  // A pointer to a variable.
  // auto = const volatile int
  auto *r1 = &x;
  // auto = volatile int
  const auto *r2 = &x;
  // auto = const int
  volatile auto *r3 = &x;
  // auto = int
  const volatile auto *r4 = &x;

  // A pointer to a function.
  // auto = void()
  auto *f1 = foo;
  // The above is equivalent to this.
  using ft = void();
  ft *f2 = foo;

  // A pointer to a C-style table.
  int t[] = {1, 2, 3};
  // auto = int[3]
  auto *t1 = &t;
  // The above is equivalent to this.
  using t3i = int[3];
  t3i *t2 = &t;
}

A regular type

Using a regular (non-reference and non-pointer) type, we can initialize a variable without putting is type. This way we can make sure the variable is initialized. Let’s remember that it’s only a trick, and not some C++ programming wisdom.

If an initializing expression is of a pointer type, then the deduced type will be pointer. In this case, initializing expressions such as a function name, an array name or a string literal would decay (into a pointer).

For a variable of a regular type, the deduced type is never reference, because the initializing expression is never of a reference type.

double foo()
{
  return .0;
}

int *goo()
{
  return static_cast<int *>(0);
}

int &loo()
{
  static int l;
  return l;
}

int
main()
{
  // auto = int
  auto w = 1;
  // auto = int
  const auto x = 2;
  // auto = int
  auto y = w;
  // auto = int
  auto z = x;

  // auto = double
  auto a = foo();
  // auto = int *
  auto b = goo();

  // auto = double (*)()
  auto fp = foo;

  int t[] = {1, 2, 3};
  // auto = int *
  auto tp = t;

  // auto = const char *
  auto hw = "Hello World!";

  // auto = int
  auto l = loo();
}

decltype

The decltype type specifier is substitued with the type of a variable or an expression that is the argument of the specifier. The type we substitute with can be any, even reference. But hold on, wasn’t it said that an expression is never of a reference type? Shouldn’t this be so for the decltype too? Well, in the case of decltype, the top-level declarator & is not removed: so the standard says.

#include <cassert>
#include <utility>

int &foo()
{
  static int i = 1;
  return i;
}

int main()
{
  double a = 0.1;
  // Variable b is of the double type.
  decltype(a) b = 1;

  int i, j = 1;
  int &x = i;

  // Variable y has the same type as variable x: an lvalue reference.
  decltype(x) y = j;
  y = 2;
  assert(j == 2);

  // Expression foo() is of the reference type to an integer, and so
  // is variable z.
  decltype(foo()) z = foo();
  z = 2;
  assert(foo() == 2);

  int &&r = 1;
  // Variable s is of the rvalue reference type to an integer.
  decltype(r) s = std::move(r);
  s = 2;
  assert(r == 2);
}

If we want the decltype speficier to yield the type of an initializing expression, then we use decltype(auto). It’s not the same as auto that employs the deduction rules for a template argument of the type kind. Here’re examples:

#include <cassert>

int &
singleton()
{
  static int i = 0;
  return i;
}

int main()
{
  // auto = int
  auto i = singleton();
  i = 1;
  assert(singleton() == 0);

  // decltype(auto) = int &
  decltype(auto) r = singleton();
  r = 1;
  assert(singleton() == 1);
}

The auto specifier in the range-based for loop

We can use the auto specifier in the range-based for loop, i.e., in the definition of the declared variable, the one available in the body of the loop. Even though using auto is handy, we do not have to use it and we can put the type explicitly. But we have to watch out, not to make a mistake.

The example below shows how easily we can make a mistake that is hard to catch. That’s a mistake that I made myself, and that I didn’t understand for a long time. In the example, the type of the declared variable is mistakenly stated: const pair<int, string> &. It looks fine, because we want to iterate using a const reference to the elements of a container, and we know that the type of the element is a pair of the key and value types. The program compiles, but does not work correctly. Where’s the mistake?

The mistake is in the first type of the pair: the container keys are const, while we requested them to be non-const. Therefore the type of the declared loop variable should be: const pair<const int, string> &. This small mistake makes the compiler create a temporary pair of (elements of types) int and string by copying the values from the pair in the container. This way we get what we wanted: a const reference to a pair of values of the requested types.

The problem is that this temporary pair soon disappears, because it’s allocated on the stack as the local data of the loop body. It’s a problem, because in the vector we store a reference to the string of the pair, and that reference dangles after an iteration, because the temporary is gone. When we output the contents of the vector, we see the same string, because the temporary pairs were created in the same place on the stack, and we see the last value.

Because in a container we cannot store a reference (const string &), then we used std::reference_wrapper<const string>. We could have used a pointer, but we can use std::reference_wrapper similar to a reference (it’s about the syntax and the semantics).

#include <iostream>
#include <functional>
#include <map>
#include <string>
#include <vector>

using namespace std;

int
main()
{
  map<int, string> m = { {1, "Alice"}, {2, "Bob"} };
  vector<std::reference_wrapper<const string>> names;

  for(const pair<int, string> &e: m)
    names.push_back(std::ref(e.second));

  for(const auto &e: names)
    cout << e.get() << endl;
}

A function return type

We can define the return type of a function using the auto specifier. In that definition we can use the qualifiers (const, volatile) and declarators (&, *).

A compiler substitutes the auto specifier with the type deduced based on the expression of the return instruction that is the initializing expression of the function result. This situation is analogous to the initialization of a function parameter in a function template, except that the function result does not have a name.

Here’re a few examples:

// auto = int[3]
auto &f1()
{
  static int t[] = {1, 2, 3};
  return t;
}

void foo()
{
};

auto *f2()
{
  return foo;
}

// auto = const int *
auto f3()
{
  static const int i = 0;
  return &i;
}

int main()
{
  // Function f1 returns an lvalue reference to a table of 3 integers,
  // and so we are able to initialize the reference below.
  int (&f1r1)[3] = f1();
  // The following is equivalent to the above.
  using f1t = int[3];
  f1t &f1r2 = f1();

  // We get a pointer to a function.
  void (*f)() = f2();

  // Here we just get a pointer to a const int.
  const int *r3 = f3();
}

Perfect returning

We’re implementing callable f that is calling some other callable g. We do not know the return type of g, but we want f to return the same data that it received from g. This is a problem of the perfect returning that is about:

Solution: the return type of f has to be the same as the return type of g. A (copy, move) constructor for the forwarded result will not be called because if g returns by:

In a correct implementation, callable f should declare its return type as decltype(auto), and the call expression of g should be the expression of the return instruction of callable f. The decltype(auto) specifier guarantees the identical return type. The auto specyfikator would deduce the type, and that we want not.

Here’s an example:

#include <iostream>

int &g1()
{
  static int i = 1;
  return i;
}

int g2()
{
  return 1;
}

template <typename G>
decltype(auto) f(G g)
{
  return g();
}

int main()
{
  f(g1) = 2;
  std::cout << g1() << std::endl;

  // Does not compile, because we can't assign to an rvalue.
  // f(g2) = 2;
}

Lambda expression and auto

In a lambda expression, we can use the auto specifier in the definition of a parameter or return type.

Parametr type

In a lambda expression we can define the parameters of the call operator using auto. Then the compiler defines a member function template for the call operator, where auto serves as the template parameter of the type kind. A call to a closure instantiates the member function template. An example:

#include <iostream>

using namespace std;

int main()
{
  auto c = [x = 0](auto i) mutable
           {
             cout << __PRETTY_FUNCTION__ << ", "
                  << "x = " << ++x << ", "
                  << "i = " << i << endl;
           };

  c(1);
  c(.1);
  c("Hello!");
}

Return type

The default return type of the call operator in a lambda is auto. Using -> we can define the return type as decltype(auto) to cater for the perfect returning.

#include <iostream>

int &
g()
{
  static int a = 1;
  std::cout << a << std::endl;
  return a;
}

int main()
{
  // auto f = []() {return g();};
  auto f = []() -> decltype(auto) {return g();};
  f() = 10;
  g();
}

Short template syntax

We can shorten a function template by its template header if we define the type of a function parameter using auto:

#include <iostream>

auto my_abs(auto t)
{
  if (t < 0)
    return -t;

  return t;
}

int main()
{
  std::cout << my_abs(-1) << std::endl;
  std::cout << my_abs(-1ll) << std::endl;
  std::cout << my_abs(-.1) << std::endl;
}

If we want two parameters to be of the same type, then auto can’t enforce it, because each auto creates a new template parameter:

#include <iostream>

auto my_max(auto a, auto b)
{
  if (a < b)
    return b;

  return a;
}

int main()
{
  std::cout << my_max(1, 2) << std::endl;

  // std::cout << my_max(1, .1) << std::endl;
}

A function template with parameter packs can also be shortened by using auto, as in the example below. In that example, however, T must be defined, because a compiler is unable to deduce its argument (there is no function parameter that uses T), and also we use it in the body of the function.

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

template <typename T>
auto
factory(auto... p)
{
  return T{p...};
}

int
main()
{
  std::cout << factory<std::string>("Hello!") << std::endl;
  auto p = factory<std::vector<int>>(1, 2, 3);
}

Conclusion

Quiz