Using auto whenever possible
Automatic type deduction is one of the most important and widely used features in modern C++. The new C++ standards have made it possible to use auto
as a placeholder for types in various contexts, letting the compiler deduce the actual type. In C++11, auto
can be used to declare local variables and for the return type of a function with a trailing return type. In C++14, auto
can be used for the return type of a function without specifying a trailing type and for parameter declarations in lambda expressions. In C++17, it can be used to declare structured bindings, which are discussed at the end of the chapter. In C++20, it can be used to simplify function template syntax with so-called abbreviated function templates. In C++23, it can be used to perform an explicit cast to a prvalue copy. Future standard versions are likely to expand the use of auto
to even more cases. The use of auto
as introduced in C++11 and C++14 has several important benefits, all of which will be discussed in the How it works... section. Developers should be aware of them and aim to use auto
whenever possible. An actual term was coined for this by Andrei Alexandrescu and promoted by Herb Sutter—almost always auto (AAA) (https://herbsutter.com/2013/08/12/gotw-94-solution-aaa-style-almost-always-auto/).
How to do it...
Consider using auto
as a placeholder for the actual type in the following situations:
- To declare local variables with the form
auto name = expression
when you do not want to commit to a specific type:auto i = 42; // int auto d = 42.5; // double auto s = "text"; // char const * auto v = { 1, 2, 3 }; // std::initializer_list<int>
- To declare local variables with the
auto name = type-id { expression }
form when you need to commit to a specific type:auto b = new char[10]{ 0 }; // char* auto s1 = std::string {"text"}; // std::string auto v1 = std::vector<int> { 1, 2, 3 }; // std::vector<int> auto p = std::make_shared<int>(42); // std::shared_ptr<int>
- To declare named lambda functions, with the form
auto name = lambda-expression
, unless the lambda needs to be passed or returned to a function:auto upper = [](char const c) {return toupper(c); };
- To declare lambda parameters and return values:
auto add = [](auto const a, auto const b) {return a + b;};
- To declare a function return type when you don’t want to commit to a specific type:
template <typename F, typename T> auto apply(F&& f, T value) { return f(value); }
How it works...
The auto
specifier is basically a placeholder for an actual type. When using auto
, the compiler deduces the actual type from the following instances:
- From the type of expression used to initialize a variable, when
auto
is used to declare variables. - From the trailing return type or the return expression type of a function, when
auto
is used as a placeholder for the return type of a function.
In some cases, it is necessary to commit to a specific type. For instance, in the first example, the compiler deduces the type of s
to be char const *
. If the intention was to have a std::string
, then the type must be specified explicitly. Similarly, the type of v
was deduced as std::initializer_list<int>
because it is bound to auto
and not a specific type; in this case, the rules say the deduced type is std::initializer_list<T>
, with T
being int
in our case. However, the intention could be to have a std::vector<int>
. In such cases, the type must be specified explicitly on the right side of the assignment.
There are some important benefits of using the auto
specifier instead of actual types; the following is a list of, perhaps, the most important ones:
- It is not possible to leave a variable uninitialized. This is a common mistake that developers make when declaring variables and specifying the actual type. However, this is not possible with
auto
, which requires an initialization of the variable in order to deduce the type. Initializing variables with a defined value is important because uninitialized variables incur undefined behavior. - Using
auto
ensures that you always use the intended type and that implicit conversion will not occur. Consider the following example where we retrieve the size of a vector for a local variable. In the first case, the type of the variable isint
, although thesize()
method returnssize_t
. This means an implicit conversion fromsize_t
toint
will occur. However, usingauto
for the type will deduce the correct type—that is,size_t
:auto v = std::vector<int>{ 1, 2, 3 }; // implicit conversion, possible loss of data int size1 = v.size(); // OK auto size2 = v.size(); // ill-formed (warning in gcc, error in clang & VC++) auto size3 = int{ v.size() };
- Using
auto
promotes good object-oriented practices, such as preferring interfaces over implementations. This is important in object-oriented programming (OOP) because it provides the flexibility to change between different implementations, modularity of the code, and better testability because it’s easy to mock objects. The fewer the number of types specified, the more generic the code is and more open to future changes, which is a fundamental principle of OOP. - It means less typing (in general) and less concern for actual types that we don’t care about anyway. It is very often the case that even though we explicitly specify the type, we don’t actually care about it. A very common case is with iterators, but there are many more. When you want to iterate over a range, you don’t care about the actual type of the iterator. You are only interested in the iterator itself; so using
auto
saves time spent typing (possibly long) names and helps you focus on actual code and not type names. In the following example, in the firstfor
loop, we explicitly use the type of the iterator. It is a lot of text to type; the long statements can actually make the code less readable, and you also need to know the type name, which you actually don’t care about. The second loop with theauto
specifier looks simpler and saves you from typing and caring about actual types:std::map<int, std::string> m; for (std::map<int, std::string>::const_iterator it = m.cbegin(); it != m.cend(); ++it) { /*...*/ } for (auto it = m.cbegin(); it != m.cend(); ++it) { /*...*/ }
- Declaring variables with
auto
provides a consistent coding style, with the type always on the right-hand side. If you allocate objects dynamically, you need to write the type both on the left and right side of the assignment, for example,int* p = new int(42)
. Withauto
, the type is specified only once on the right side.
However, there are some gotchas when using auto
:
- The
auto
specifier is only a placeholder for the type, not for theconst
/volatile
and reference specifiers. If you need aconst
/volatile
and/or a reference type, then you need to specify them explicitly. In the following example, theget()
member function offoo
returns a reference toint
; when the variablex
is initialized from the return value, the type deduced by the compiler isint
, notint&
. Therefore, any change made tox
will not propagate tofoo.x_
. In order to do so, we should useauto&
:class foo { int x; public: foo(int const value = 0) :x{ value } {} int& get() { return x; } }; foo f(42); auto x = f.get(); x = 100; std::cout << f.get() << '\n'; // prints 42
- It is not possible to use
auto
for types that are not moveable:auto ai = std::atomic<int>(42); // error
- It is not possible to use
auto
for multi-word types, such aslong long
,long double
, orstruct foo
. However, in the first case, the possible workarounds are to use literals or type aliases; also, with Clang and GCC (but not MSVC) it’s possible to put the type name in parentheses,(long long){ 42 }
. As for the second case, usingstruct
/class
in that form is only supported in C++ for C compatibility and should be avoided anyway:auto l1 = long long{ 42 }; // error using llong = long long; auto l2 = llong{ 42 }; // OK auto l3 = 42LL; // OK auto l4 = (long long){ 42 }; // OK with gcc/clang
- If you use the
auto
specifier but still need to know the type, you can do so in most IDEs by putting the cursor over a variable, for instance. If you leave the IDE, however, that is not possible anymore, and the only way to know the actual type is to deduce it yourself from the initialization expression, which could mean searching through the code for function return types.
The auto
can be used to specify the return type from a function. In C++11, this requires a trailing return type in the function declaration. In C++14, this has been relaxed, and the type of the return value is deduced by the compiler from the return
expression. If there are multiple return values, they should have the same type:
// C++11
auto func1(int const i) -> int
{ return 2*i; }
// C++14
auto func2(int const i)
{ return 2*i; }
As mentioned earlier, auto
does not retain const
/volatile
and reference qualifiers. This leads to problems with auto
as a placeholder for the return type from a function. To explain this, let’s consider the preceding example with foo.get()
. This time, we have a wrapper function called proxy_get()
that takes a reference to a foo
, calls get()
, and returns the value returned by get()
, which is an int&
. However, the compiler will deduce the return type of proxy_get()
as being int
, not int&
.
Trying to assign that value to an int&
fails with an error:
class foo
{
int x_;
public:
foo(int const x = 0) :x_{ x } {}
int& get() { return x_; }
};
auto proxy_get(foo& f) { return f.get(); }
auto f = foo{ 42 };
auto& x = proxy_get(f); // cannot convert from 'int' to 'int &'
To fix this, we need to actually return auto&
. However, there is a problem with templates and perfect forwarding the return type without knowing whether it is a value or a reference. The solution to this problem in C++14 is decltype(auto)
, which will correctly deduce the type:
decltype(auto) proxy_get(foo& f)
{ return f.get(); }
auto f = foo{ 42 };
decltype(auto) x = proxy_get(f);
The decltype
specifier is used to inspect the declared type of an entity or an expression. It’s mostly useful when declaring types is cumbersome or if they can’t be declared at all with the standard notation. Examples of this include declaring lambda types and types that depend on template parameters.
The last important case where auto
can be used is with lambdas. As of C++14, both lambda return types and lambda parameter types can be auto
. Such a lambda is called a generic lambda because the closure type defined by the lambda has a templated call operator. The following shows a generic lambda that takes two auto
parameters and returns the result of applying operator+
to the actual types:
auto ladd = [] (auto const a, auto const b) { return a + b; };
The compiler-generated function object has the following form, where the call operator is a function template:
struct
{
template<typename T, typename U>
auto operator () (T const a, U const b) const { return a+b; }
} L;
This lambda can be used to add anything for which the operator+
is defined, as shown in the following snippet:
auto i = ladd(40, 2); // 42
auto s = ladd("forty"s, "two"s); // "fortytwo"s
In this example, we used the ladd
lambda to add two integers and concatenate them to std::string
objects (using the C++14 user-defined literal operator ""s
).
See also
- Creating type aliases and alias templates, to learn about aliases for types
- Understanding uniform initialization, to see how brace-initialization works