GotW #92 Solution: Auto Variables, Part 1

What does auto do on variable declarations, exactly? And how should we think about auto? In this GotW, we’ll start taking a look at C++’s oldest new feature.

 

Problem

JG Questions

1. What is the oldest C++11 feature? Explain.

2. What does auto mean when declaring a local variable?

Guru Questions

3. In the following code, what is the type of variables a through k, and why? Explain.

int         val = 0;
auto a = val;
auto& b = val;
const auto c = val;
const auto& d = val;

int& ir = val;
auto e = ir;

int* ip = &val;
auto f = ip;

const int ci = val;
auto g = ci;

const int& cir = val;
auto h = cir;

const int* cip = &val;
auto i = cip;

int* const ipc = &val;
auto j = ipc;

const int* const cipc = &val;
auto k = cipc;

4. In the following code, what type does auto deduce for variables a and b, and why? Explain.

int val = 0;

auto a { val };
auto b = { val };

 

Solution

1. What is the oldest C++11 feature? Explain.

auto x = something; to declare a new local variable whose type is deduced from something, and isn’t just always int.

Bjarne Stroustrup likes to point out that auto for deducing the type of local variables is the oldest feature added in the 2011 release of the C++ standard. He implemented it in C++ 28 years earlier, in 1983—which incidentally was the same year the language’s name was changed to C++ from C with Classes (the new name was unveiled publicly on January 1, 1984), and the same year Stroustrup added other fundamental features including const (later adopted by C), virtual functions, & references, and BCPL-style // comments.

Alas, Stroustrup was forced to remove auto because of compatibility concerns with C’s then-existing implicit int rule, which has since been abandoned in C. We’re glad auto is now back and here to stay.

2. What does auto mean when declaring a local variable?

It means to deduce the type from the expression used to initialize the new variable. In particular, auto local variables deduction is exactly the same as type deduction for parameters of function templates—by specification, the rule for auto variables says “do what function templates are required to do”—plus they can capture initializer_list as a type. For example:

template<class T> void f( T ) { }

int val = 0;

f( val ); // deduces T == int, calls f<int>( val )
auto x = val; // deduces T == int, x is of type int

When you’re new to auto, the key thing to remember is that you really are declaring your own new local variable. That is, “what’s on the left” is my new variable, and “what’s on the right” is just its initial value:

auto my_new_variable = its_initial_value;

You want your new variable to be just like some existing variable or expression over there, and be initialized from it, but that only means that you want the same basic type, not necessarily that other variable’s own personal secondary attributes such as top-level const– or volatile-ness and &/&& reference-ness which are per-variable. For example, just because he’s const doesn’t mean you’re const, and vice versa.

It’s kind of like being identical twins: Andy may be genetically just like his brother Bobby and is part of the same family, but he’s not the same person; he’s a distinct person and can make his own choice of clothes and/or jewelry, go to be seen on the scene in different parts of town, and so forth. So your new variable will be just like that other one and be part of the same type family, but it’s not the same variable; it’s a distinct variable with its own choice of whether it wants to be dressed with const, volatile, and/or a & or && reference, may be visible to different threads, and so forth.

Remembering this will let us easily answer the rest of our questions.

3. In the following code, what is the type of variables a through k, and why? Explain.

Quick reminder: auto means “take exactly the type on the right-hand side, but strip off top-level const/volatile and &/&&.” Armed with that, these are mostly pretty easy.

For simplicity, these examples use const and &. The rules for adding or removing const and volatile are the same, and the rules for adding or removing & and && are the same.

int         val = 0;
auto a = val;
auto& b = val;
const auto c = val;
const auto& d = val;

For a through d, the type is what you get from replacing auto with int: int, int&, const int, and const int&, respectively. The same ability to add const applies to volatile, and the same ability to add & applies to &&. (Note that && will be what Scott Meyers calls a universal reference, just as with templates, and does in some cases bring across the const-ness if it’s binding to something const.)

Now that we’ve exercised adding top-level const (or volatile) and & (or &&) on the left, let’s consider how they’re removed on the right. Note that the left hand side of a through d can be used in any combination with the right hand side of e through k.

int&        ir  = val;
auto e = ir;

The type of e is int. Because ir is a reference to val, which makes ir just another name for val, it’s exactly the same as if we had written auto e = val; here.

Remember, just because ir is a reference (another name for the existing variable val) doesn’t have any bearing on whether we want e to be a reference. If we wanted e to be a reference, we would have said auto& as we did in case b above, and it would have been a reference irrespective of whether ir happened to be a reference or not.

int*        ip  = &val; 
auto f = ip;

The type of f is int*.

const int   ci  = val;
auto g = ci;

The type of g is int.

Remember, just because ci is const (read-only) doesn’t have any bearing on whether we want g to be const. It’s a separate variable. If we wanted g to be const, we would have said const auto as we did in case c above, and it would have been const irrespective of whether ci happened to be const or not.

const int&  cir = val;
auto h = cir;

The type of h is int.

Again, remember we just drop top-level const and & to get the basic type. If we wanted h to be const and/or &, we could just add it as shown with b, c, and d above.

const int*  cip = &val;
auto i = cip;

The type of i is const int*.

Note that this isn’t a top-level const, so we don’t drop it. We pronounce cip‘s declaration right to left: The type of cip is “pointer to const int,” not “const pointer to int.” What’s const is not cip, but rather *cip, the int it’s pointing to.

int* const  ipc = &val;
auto j = ipc;

The type of j is int*. This const is a top-level const, and ipc‘s being const is immaterial to whether we want j to be const.

const int* const cipc = &val;
auto k = cipc;

The type of k is const int*.

4. In the following code, what type does auto deduce for variables a and b, and why? Explain.

As we noted in #2, the only place where an auto variable deduces anything different from a template parameter is that auto deduces an initializer_list. This brings us to the final cases:

int val = 0;

auto a { val };
auto b = { val };

The type of both a and b is std::initializer_list<int>.

That’s the only difference between auto variable deduction and template parameter deduction—by specification, because auto deduction is defined in the standard as “follow those rules over there in the templates clause, plus deduce initializer_list.”

If you’re familiar with templates and curious how auto deduction and template deduction map to each other, the table below lists the main cases and shows the equivalent syntax between the two features. For the left column, I’ll put the variable and the initialization on separate lines to emphasize how they correspond to the separated template parameter and call site on the right.

Not only are the cases equivalent in expressive power, but you might even feel that some of the auto versions feel even slicker to you than their template counterparts.

Summary

Having auto variables really brings a feature we already had (template deduction) to an even wider audience. But so far we’ve only seen what auto does. The even more interesting question is how to use it. Which brings us to our next GotW…

Acknowledgments

Thanks in particular to the following for their feedback to improve this article: davidphilliposter, Phil Barila, Ralph Tandetzky, Marcel Wild.

19 thoughts on “GotW #92 Solution: Auto Variables, Part 1

  1. I really don’t understand why f is a int* while h is int. That doesn’t make sense to me. Anybody can explain? To me I don’t see any common rule to this.

    Thanks

  2. The first letter of the explanatory paragraph after:
    “The type of i is const int*.”
    is a stray ‘s’.

  3. @monamimani
    Take a look at what Herb wrote about e. Similar reasoning applies to h. cir is a reference to val and it would not allow to change val. But

    auto h = cir;
    

    is equivalent to

    auto h = val;
    

    In the case of f ip is not a different name for val. It’s a pointer to a location in memory, it contains an address.

    auto f = ip;
    

    assigns to f the address contained in ip, not the integer contained in val.

  4. Maybe you should mention that volatile is also stripped from the right hand side type for completeness. This highlights the symmetry of const and volatile. Are there any other things that can be stripped? In the second section you say “such as top-level const-ness and reference-ness”. Instead you could say “which are top-level const-ness, volatile-ness and reference-ness”. Just a thought.

  5. @Herb,
    The table you mentioned for question 4 is only visible in this post in your website, but not in the post I received in my inbox. I wonder why?

  6. @Herb
    did you consider putting donate button on your site… I personally dont really buy books, but I appreciate your work with refresnihg GotW and new GotWs and I would like to have option to show my appreciation with my euros. :D
    IDK if other ppl agree so this is just IMO :D

  7. It might be worth adding the following examples. After reading this article I felt the need to test these cases since auto expands to “const int” which does have a top-level const even though the full types of g2 and h2 don’t.

    const int   ci  = val;
    auto        g   = ci;
    auto&       g2  = ci;
    
    const int&  cir = val;
    auto        h   = cir;
    auto&       h2  = cir;
    

    Also, I’m curious about auto&&. I think it would only be an rvalue-ref when the right hand side was a value or rvalue-ref returned from a function. Is there any detail I’m missing?

  8. I like to think about auto type deduction in terms of template argument deduction (@Herb: You should replace “parameter” by “argument” in the heading of the 2nd column). The above table may be extended to

    auto         x = expr;    template<class U> void f(U t);         f(expr); //(1)
    const auto   x = expr;    template<class U> void f(const U t);   f(expr); //(2)
    auto&        x = expr;    template<class U> void f(U& t);        f(expr); //(3)
    const auto&  x = expr;    template<class U> void f(const U& t);  f(expr); //(4)
    auto&&       x = expr;    template<class U> void f(U&& t);       f(expr); //(5)
    const auto&& x = expr;    template<class U> void f(const U&& t); f(expr); //(6)
    

    If expr has type “reference to T”, then this type is adjusted to “T” prior to any further analysis. This has nothing to do with auto, this is a general rule in C++. For auto type deduction this means that we can ignore any
    reference-ness on expr. We denote this adjusted type of expr by A. To further analyse what’s going on behind the scene, it is comfortable to denote the parameter type of f by P, for example
    in case (4) above P is “const U&”, in case (5) it is “U&&”. Now we have to distinguish between the following cases:
    – P is not a reference type (cases (1) and (2) above):
    1. If A is an array or function type, A is replaced by the pointer resulting from array-to-pointer or function-to-pointer conversion

    int a[7];
    const int b[7];
    auto x = a;
    auto y = b;
    

    then x is of type int* and y of type const int*.
    2. If A is a cv-qualified type, the the cv-qualifiers are ignored.

    const int& cir = 0;
    auto         h = cir;
    

    Here cir has type “reference to const int”. This is adjusted to “const int” (=A) and now const is ignored and the type of h is int.
    – If P is cv-qualified the top-level cv-qualifiers are ignored for type deduction (case (2) above).

    int& ir      = 0;
    const auto x = ir;
    

    Here again, & is dropped from ir and const is ignored, so we have to determine U such that “f(int) = f(U)”, hence “U = int” and the type of x is const int.
    Now it gets a bit more involved:
    – If P is a reference type, type referred to by P is used for type deduction (and if this resulting type is cv-qualified, the cv-qualifiers are ignored). If P is an rvalue reference to a cv-unqualified template parameter
    and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.

    const double& rd = 1.0;
    int&&         ri = 0;
    auto&&         x = rd;
    auto&&         y = ri;
    

    Here we are in case (5). P is an rvalue reference to a cv-unqualified template parameter (U&&) and the argument is an lvalue (rd), so A (which is “const int”, note that here const is not dropped!)
    gets replaced by “const double&” and U is deduced to be “const double&”. Now reference collapsing comes in (& && becomes &) and x is of type const double&. Similarly, y is of type int& (note that ri is an lvalue).

    int&&         ri = 0;
    const auto&&   y = ri;
    

    Here we are in case (6) and get a compiler error. Why? P is an rvalue reference to a cv-QUALIFIED template parameter. After dropping & and const from P (A is int), we have to find U such that “f(U) = f(int)”. Hence “U = int”.
    The type of y becomes “const int&&”, but binding an lvalue (ri) to an rvalue reference is not allowed.

    const int& cir = 0;
    auto&        x = cir;
    

    Here we are in case (3). A is “const int” and we have to deduce U such that “f(const int) = f(U)”, thus “U = const int”. The type of x is const int&. I hope, this helps clarifying how auto works.

  9. @Phil: Fixed, thanks.

    @Ralph: Good suggestion. I was trying to keep it simple, but volatile and && should be mentioned up front. Updated.

    @nosenseetal: That’s very kind, thank you, but I don’t accept donations. I’ve also never run any ads. I have always published my GotW and other articles for free online, and only the final print/e-book versions have been for sale as they’re done through a commercial magazine or book publisher. (Magazines, remember those? How quaint! We do miss C++ Report…)

    @Marcel: Thanks, and I’ve adjusted the layout of the table to make things clearer.

  10. Great article as usual.
    I just found a bit confusing the example for the case of “h”.
    If I’m correct, const T& is not a case of top-level const, so in case “h” what happens is actually that reference is stripped off _AND THEN_ what remains is a top level const that gets stripped as such. Am I right?

  11. @Leo Sorry for being late to reply. It still doesn’t make sense to me and I’ll point out to that the template argument deduction rule doesn’t either.

    To me the type of cir it a reference to a int the type of cir is not a int. Ref to int a and int are 2 different types. Then it confuses me that the deduced type it not int&.

  12. @Herb, thanks for the article!
    2 questions as I didn’t chase it closely:
    1) Will auto pick type attributes like alignment? The code I mean is smth like:

    typedef float aligned_block[4] alignas(16);
    aligned_block a{...};
    auto b = a; // is b aligned on 16?
    

    2) what about explicit auto? Is there any protection for private classes that we don’t want to expose (e.g. because it holds references to temporary objects from the enclosing full expression)?

Comments are closed.