Some may argue that what is unfair is to expect someone to learn auto type deduction, and I am willing to concede that figuring out the type deduced by some type specifiers containing auto can be a bit daunting. There are five sets of rules each to be used in different situations. If that wasn’t enough, auto deduces its types from initializing expressions, and occasionally, these expressions have unexpected types, which are not what you want. On the other hand, auto has many benefits. Using auto to declare variables guarantees no implicit conversions, no temporary variables, no narrowing conversions, and no uninitialized variables. They can directly hold closures. The use of auto also prevents some performance issues and often results in more concise declarations with less duplication. This makes the code easier to read and more maintainable, not to mention there is less typing to do. The list goes on, but the question remains: is it worth learning auto type deduction in order to reap auto’s benefits? Or is learning it so difficult and confusing, so unteachable, that it is better to discourage its use in order to avoid bugs by less experienced developers? To answer this question, we need to look at the rules; below is a summary. I know, I know, rules are boring, but bear with me. By the end of this article, in addition to looking at my case, you may be well on your way to knowing auto type deduction.

Auto type deduction covers three use cases:

a variable is declared using auto (C++11), auto is used as a function return type (C++14), and auto is used in a lambda’s parameter declaration(C++14).

There is also another use case that is not strictly “auto type deduction,” but since it contains the word “auto,” I’ll cover it for completeness:

decltype(auto) is used as a function return type (C++14)

Let’s examine each one of them in turn.

Use Case 1. A variable is declared using auto (C++11)

When a variable is declared using auto, there are three parts of the declaration to identify in order to figure out the variable’s type: the Type Specifier, the word auto, and the Initializing Expression. In the following declaration,

const auto& var = i;

the type specifier is “const auto&” and the initializing expression is “i”. The word “auto” is the word “auto”, of course, and “var” is the name of the variable which type we are trying to deduce. In this other variable declaration,

auto i = 0;

the word “auto” is also the type specifier, and the initializing expression is “0”.

Once you have identified these parts, you must follow one of the rule sets below in order to determine the variable’s type. Which set to follow depends on the type specifier.

Rule Set 1: Follow if type specifier is a reference or pointer, but not a universal/forwarding reference

If the initializing expression is a reference, ignore the reference part. Pattern-match the initializing expression type to the type specifier to determine the type “auto” represents. After this you should have the variable type also.

For example:

int x = 0; int& r = x; const int& cr = x; const auto & v = x; // v’s type is “const int&”; auto is “int" const auto & cv = cr; // cv’s type is “const int&”; auto is “int" auto * w = &r; // w’s type is “int*”; auto is “int" auto * cw = &cr; // cw’s type is “const int*”; auto is “const int” std::vector<int> vec({1, 2, 3, 4, 5}); auto & first = vec[0]; // first's type is "int&”; auto is “int”

Rule Set 2: Follow if type specifier is a universal/forwarding reference (i.e. auto&&)

If the initializing expression is an lvalue, both “auto” and the type specifier are deduced to be lvalue references, so pattern-match the initializing expression to the type specifier and append an lvalue reference if it isn’t already. If the initializing expression is an rvalue, Set 1 rules apply.

For example:

int x = 0; int& r = x; const int c = x; const int& cr = x; // initializing expression is an lvalue auto && v = x; // v’s type is “int&”; auto is “int&” auto && cv = cr; // cv’s type is “const int&”; auto is “const int&” auto && cw = c; // cw’s type is “const int&”; auto is “const int&” auto && z = r; // z’s type is “int&”; auto is “int&" // initializing expression is an rvalue auto && s = "hello world!" s ; // s’s type is "string&&" // auto is "string" auto && max = 10; // max’s type is "int&&"; auto is "int"

Rule Set 3: Follow if type specifier is neither a pointer nor a reference

If the initializing expression is an array, it decays to a pointer to its first element. If the initializing expression is a function, it decays to a function pointer. If the initializing expression is a reference, ignore the reference part. If the initializing expression is const, ignore the const part. If the initializing expression is volatile, ignore the volatile part. Pattern-match the initializing expression type to the type specifier to determine the type “auto” represents.

Here are some examples:

auto x = 0; // x’s type is “int” int& r = x; const int& cr = x; auto y = cr; // y’s type is “int” const auto cy = r; // cy’s type is “const int”; auto is “int” const int c[5] = {0, 1, 2, 3, 4}; auto a = c; // a’s type is “const int*” int f(int i, int j) { ... } ... auto g = f; // g’s type is int (*)(int, int)

At this point, you are probably thinking: “Holy cow! All these rules, and he is not even done with Use Case 1!” Well, I have good news. Yes I’m not finished with Use Case 1, but these three rule sets that I’ve covered so far are the same sets for all other auto use cases. Use Case 1 is special though because it has an extra rule:

Initializer List Rule :

When the initializer for an auto-declared variable is enclosed in braces, the deduced type is a std::initializer_list. This rule is simple, but it causes confusion for beginners because it is inconsistent with declaring a variable with a type using C++11’s Uniform Initialization. For example:

int i = 10; int j(10); int k = {10}; // C++11 uniform initialization int l{10}; // C++11 uniform initialization

The result for all four syntaxes above is an int with value 10. However, according to auto’s Initializer List rule:

auto i = 10; // i’s type is int, and it’s value is 10 auto j(10); // j’s type is int, and it’s value is 10 auto k = {10}; // k’s type is std::initializer_list<int>, // and its value is {10} auto l{10}; // l’s type is std::initializer_list<int>, // and its value is {10}

The type of k and l in the last two declarations is std::initializer_list<int>, and their value is {10}. This is true up to C++14. After C++14, l’s type will be int with a value of 10 since the C++ Standardization Committee adopted proposal N3922, which eliminates auto’s Initializer List rule for l but not for k. Some compilers, clang++ and g++ in particular, have implemented N3922 even when compiling with their C++11 or C++14 options, so be in the lookout for this.

This is it, the end of Use Case, 1. As I’ve already mentioned, it covers all auto type deduction rules. The following two use cases, 2 and 3, require the same set of rules except for the Initializer List Rule, which only applies to variables declared using auto, Use Case 1.

Use Case 2. auto is used as a function return type (C++14)

It is the same as Use Case 1 except the Initializer List rule doesn’t apply, and instead of using the initializing expression to deduce the return type, the return expression’s type is used. If there is more than one return statement, they should all have the same type, or the compiler will complain. Below are a few examples:

auto f(void) { int result = 0; ... return result; // f’s return type is int } auto g(void) { ... return {0, 1, 2, 3}; // error; can’t deduce type; // Initializer List rule doesn’t apply } auto h(vector<int>& v, int i) { ... return v[i] ; // h’s return type is int }

In the last example, h() returns an int, which is likely not what you want. The type for vector<int>::operator[] is int&, but according to Rule Set 3, the reference part will be ignored when deducing the type. Therefore, if what you want is int&, you must do something different, and Use Case 4 is a good option. Before we get to it, however, let’s address the last “real” auto use case.

Use Case 3. auto is used in a lambda’s parameter declaration (C++14)

Again, it is the same as Use Case 1 except the Initializer List rule doesn’t apply. Also, the lambda’s parameter type takes the place of the type specifier, and the parameter expression in the closure call takes the place of the initializing expression. In other words, it works as with templates with the word “auto” taking the place of “T” (or whatever your template parameter name is). It sounds a bit confusing, but it isn’t. It is just easier to understand it with a couple of examples:

int i = -1; const int& cr = i; unsigned ui = 0; ... auto f = []( auto x) { return (0 < x) && (x < 10); }; // parameter type = auto ... f( i ); // parameter expression is i; x’s type is int f( cr ); // parameter expression is cr; x’s type is int f( ui ); // parameter expression is ui; x’s type is unsigned ... std::list<int> l; ... auto append = [&]( const auto& value) { l.insert(l.end(), value); }; append( {1, 2, 3} ); // error! can’t deduce type for {1, 2, 3}

Use Case 4. decltype(auto) is used as a function return type (not really auto type deduction) (C++14)

decltype(auto) means deduce the type (from the return type), but use decltype deduction rules. The following are decltype type deduction rules:

For a name, decltype(name) always deduces its declared type. For expressions, decltype(expr): if expr is a prvalue , decltype(expr) deduces its type.

If expr is an xvalue , decltype(expr) deduces an rvalue reference to the expression’s type.

If expr is an lvalue, it deduces an lvalue reference to the expression’s type.

Here are a couple of examples for the first two bullets:

// expr is a prvalue decltype(auto) f() { int r = 0; return(r+5); // (r+5) is a prvalue; f() returns "int" } // expr is an xvalue decltype(auto) g() { int r = 0; return std::move(r); // move() returns xvalue; g() returns "int&&" }

For an example of expr being an lvalue, let’s get back to Use Case 2’s third example, and modify h() to return a reference to the value of the vector at index i:

// C++14 decltype(auto) h(vector<int>& v, int i) { ... return v[i] ; // h’s return type is now "int&" }

Note: be careful with returning a name in parenthesis because it becomes an expression, and expressions are always deduced as lvalue references by decltype. For example:

decltype(auto) f() { int r = 0; ... return r; // “r” is a name, so f() returns an “int” } // however… decltype(auto) g() { int r = 0; ... return(r); // “(r)” is an expression, so g() returns an “int&” }

As you see, adding the parentheses to the return statement in g() causes it to return a different type from f(). Not only that, but g() is returning a reference to a local variable, which will go out of scope when the function returns. This is not what you want.

Before wrapping it up, let me address another use of decltype() type deduction rules, C++11 trailing return type syntax.

Look at the following function:

// C++11 trailing return type syntax auto h(vector<int>& v, int i) -> decltype(v[i]) // h() return type // is int& { ... }

Here again, auto has nothing to do with type deduction, and the function return type is deduced using decltype() rules. The advantage of trailing return type syntax is that the function parameters may be used to specify the return type. The trailing return type syntax, however, is not as useful in C++14, and therefore, this is all I’m going to say about it.

Finally! There you have them. All of auto type deduction rules and when to use them. There are three sets of rules and the Initializer List rule for three different uses of auto, plus the decltype(auto) use case. Yes, I also wish that there was only one set of rules for all of the use cases, but life is not always so simple. I also felt confused and made some mistakes at first, but learning these rules is very doable. They can be mastered with some practice, and then auto may be used with a high level of confidence.

In my opinion, auto was a valuable addition to the C++ standard. Herb Sutter goes as far as to propose to always declare variables using auto. Yes, even when declaring an int: “auto i = 0;”. You may not be prepared to go that far, but you should at least look at auto as a valuable tool in your toolbox. Below is an example from Effective Modern C++ by Scott Meyers that made me better appreciate the advantages of auto:

std::unordered_map<std::string, int> m; ... for (const std::pair<std::string, int>& p : m) { ... }

Can you see the problem? The value_type for m is std::pair<const std::string, int> not std::pair<std::string, int> (the string is const). The effects of these types being different will cause this code to run much slower because each iteration through the loop, a temporary variable of the specified type will be copy-constructed from each element in the unordered_map so that p can be bound to it. Also, on each iteration, the temporary variable will be destroyed by calling its destructor. Obviously, it would be faster to just bound p to each of the existing elements instead of making a copy of each element in the map and then destroying it. This error would have been avoided if auto had been used:

for (const auto& p : m) { ... }

This example resonated with me because I had a similar experience in the past. One of our products was having performance issues, and I was asked to figure out why. There were several problems, but some of them had to do with the optimized code path not being followed because overloaded functions were being called using types with the wrong const-ness. Similarly to the example shown above, those problems wouldn’t have occurred if auto had been used.

Finally, I’m going to address the “it is unfair” portion of the title. Type deduction rules are used in many contexts in C++11 and C++14. In addition to enabling us to get the benefits of auto, developers need to know them in order to minimize confusion and to understand well many modern C++ features. They are necessary to program effectively in modern C++. Therefore, limiting the use of auto also limits developers improvement and their understanding of Modern C++. I’ve always had the belief that instead of restricting the use of a feature because most don’t understand it, we should help our fellow developers to learn it. It makes us all better, and in the end, it helps our companies succeed. Win-win, shouldn’t that be the goal?

SIGN UP TO MY EMAIL LIST!

If you liked this article, SIGNUP TO MY EMAIL LIST, and I’ll email you when I post anything new. My aim is to create material that:

Is practical so that you can start using it NOW.

Will guide you on what to focus and what not to so that you can learn Modern C++ FAST.

Will refresh important knowledge in order for you to remain SUCCESSFUL.



