Auto in C++ - Basics

Friday, June 07, 2013 , 0 Comments

The auto Keyword: The Basics
Consider the following code snippet:
std::vector<int> vect;
// ...

  std::vector<int>::const_iterator cit = vect.begin();
  cit != vect.end();
  std::cout << *cit;
Perfectly fine, we all write code like this or similar all the time. Now let us consider this instead:
std::vector<int> vect;
// ...

  std::vector<int>::iterator it = vect.begin();
  it != vect.end();
  std::cin >> *it;
When writing this code, you should be getting slightly annoyed. The variable it is of the exact same type as the expression vect.begin() with which it is being initialized, and the compiler knows what that type is. Therefore, for us to have to write std::vector<int>::iterator is redundant. The compiler should use this as the default for the type of the variable it. And that is exactly the issue that the auto keyword addresses. In C++11, we can write:
std::vector<int> vect;
// ...

for(auto it = vect.begin(); it != vect.end(); ++it)
  std::cin >> *it;
The compiler will use the type of the initializing expression vect.begin(), which is std::vector<int>::iterator, as the type of the variable it. This sounds easy enough, but it's not quite the full story on auto. Read on.
The auto Keyword: The Rest of the Story
Consider this example of using auto:
int x = int(); // x is an int, initialized to 0
assert(x == 0);

const int& crx = x; // crx is a const int& that refers to x
x = 42;
assert(crx == 42 && x == 42);

auto something = crx;
The crucial question is, what is the type of something? Since the declared type of crx is const int&, and the initializing expression for something is crx, you might think that the type of something is const int&. Not so. It turns out that the type of something is int:
assert(something == 42 && crx == 42 && x == 42);
// something is not const:
something = 43;
// something is not a reference to x:
assert(something == 43 && crx == 42 && x == 42);
Before we discuss the rationale behind this behavior, let us state the exact rules by which autoinfers the type from an initializing expression:

When auto sets the type of a declared variable from its initializing expression, it proceeds as follows:
  1. If the initializing expression is a reference, the reference is ignored.
  2. If, after Step 1 has been performed, there is a top-level const and/or volatile qualifier, it is ignored.

As you have probably noticed, the rules above look like the ones that function templates use to deduce the type of a template argument from the corresponding function argument. There is actually a small difference:auto can deduce the type std::initializer_list from a C++11-style braced list of values, whereas function template argument deduction cannot. Therefore, you may use the rule "auto works like function template argument deduction" as a first intuition and a mnemonic device, but you need to remember that it is not quite accurate.Continuing with the example above, suppose we pass the const int& variable crx to a function template:
template<class T>
void foo(T arg);
Then the template argument T resolves to int, not to const int&. So for this instantiation of foo, the argument arg is of type int, not const int&. If you want the argument arg of foo to be a const int&, you can achieve that either by specifying the template argument at the call site, like this:
foo<const int&>(crx);
or by declaring the function like this:
template<class T>
void foo(const T& arg);
The latter option works analogously with auto:
const auto& some_other_thing = crx;
Now some_other_thing is a const int&, and that is of course true regardless of whether the initializing expression is an int, an int&, a const int, or a const int&.
assert(some_other_thing == 42 && crx == 42 && x == 42);
some_other_thing = 43;  // error, some_other_thing is const
x = 43;
assert(some_other_thing == 43 && crx == 43 && x == 43);
The possibility of adding a reference to the auto keyword as shown in the example above requires a small amendment to auto's const-stripping behavior. Consider this example:
  const int c = 0;
  auto& rc = c;
  rc = 44; // error: const qualifier was not removed
If you went strictly by the rules stated earlier, auto would first strip the const qualifier off the type of c, and then the reference would be added. But that would give us a non-const reference to the const variable c, enabling us to modify c. Therefore, auto refrains from stripping the const qualifier in this situation. This is of course no different from what function template argument deduction does.
Let's do one more example to demonstrate that auto drops const and volatilequalifiers only if they're at the top or right below an outermost reference:
int x = 42;
const int* p1 = &x;
auto p2 = p1;
*p2 = 43; // error: p2 is const int*
Now that we know how auto works, let's discuss the rationale behind the design. There is probably more than one way to argue. Here's my way to see why the behavior is plausible: being a reference is not so much a type characteristic as it is a behavioral characteristic of a variable. The fact that the expression from which I initialize a new variable behaves like a reference does not imply that I want my new variable to behave like a reference as well. Similar reasoning can be applied to constness and volatility1. Therefore,auto does not automatically transfer these characteristics from the initializing expression to my new variable. I have the option to give these characteristics to my new variable, by using syntax like const auto&, but it does not happen by default.