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:
a variable,
a parameter of a function,
a parameter of a lambda expression,
what a function returns.
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.
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 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:
auto is treated like the name of a template parameter of the type
kind,
the initializing expression is treated like the function argument.
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.
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);
}
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;
}
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();
}
decltypeThe 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);
}
auto specifier in the range-based for loopWe 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;
}
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();
}
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:
preventing the copying or moving of the data,
keeping the category of the call expression of g, i.e., the call
expression of f should have the same category.
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:
reference, then f returns by reference of the same type
(lvalue reference, const reference, rvalue reference), and such
reference initialization does not call a constructor,
value, then f returns by value of the same type, and then a
constructor is elided.
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;
}
autoIn a lambda expression, we can use the auto specifier in the
definition of a parameter or return 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!");
}
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();
}
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);
}
The auto specifier allows for type deduction.
In a for loop it’s best to use auto.
In a lambda expression, auto is handy.
Where can we use auto?
What’s the difference between specifiers auto and
decltype(auto)?
What is the perfect returning about?