When auto Seemingly Deduces a Reference in C++

One of the first things that C++ programmers learn about auto is that bare auto never deduces a reference. In the present post, I show you two examples when it seemingly does. The first one involves proxy objects. The second one concerns structured bindings from C++17.

Auto Type Deduction

Just to refresh your memory, when you use auto, the compiler determines the type that will replace the auto keyword by using the rules for template type deduction from a function call. Among other things, this basically means that when you write a bare auto, you get a copy:

int x = 0;
auto y = x;
y = 1;
std::cout << x << ' ' << y << '\n'; // 0 1

and when you write auto&, you get a reference:

int x = 0;
auto& y = x;
y = 1;
std::cout << x << ' ' << y << '\n'; // 1 1

Sounds simple. Right? Well, it kind of is, but also kind of not. There are situations when a bare auto seemingly deduces a reference.

First Example (Proxy Objects)

Consider the following piece of code:

std::vector<int> v{0, 0, 0};
auto x = v[1];
x = 1;
std::cout << v[0] << ' ' << v[1] << ' ' << v[2] << '\n';

We create a vector of ints holding three zeros, assign the second element into a new variable x, change x, and then print the contents of the vector. Since we have used a bare auto, the type of x will be int, so we get a copy. Therefore, the code prints 0 0 0, as you would expect. Easy peasy.

Pop quiz: If we switch the type from int to bool, what does the following code print?

std::vector<bool> v{0, 0, 0};
auto x = v[1];
x = 1;
std::cout << v[0] << ' ' << v[1] << ' ' << v[2] << '\n';

Surprisingly, it prints 0 1 0. The reason is that a vector of bools is special. It is a partial specialization of std::vector<T, Allocator> that should be space-efficient. Instead of storing each bool in a single byte, it might coalesce the elements such that each element occupies a single bit. However, since C++ does not allow taking the address of a bit within a byte, methods such as operator[] cannot return bool&. Instead, they return a so-called proxy object that allows to manipulate the particular bit. Internally, a proxy object can contain a pointer to the vector and the number of the bit (e.g. the second bit in case of v[1]). Therefore, in our code above, v[1] returns a proxy object, which, although copied into x, contains a pointer to the vector. So, any operation with the proxy object operates with the vector.

In general, proxy objects are useful in a variety of situations, and classes that you use may employ them under the covers. Thus, keep in mind that when you see a bare auto, do not blindly assume that you can do whatever you want with the copy without changing something.

Alright, let’s move to the second example.

Second Example (Structured Bindings)

In what follows, I assume that you are familiar with structured bindings from C++17. If you are not, I encourage you to read about them. They are useful.

Consider the following piece of code:

std::pair p{0, 0};
auto [x, y] = p;
y = 1;
std::cout << p.first << ' ' << p.second << '\n';

First, we create a pair of two integers. Note that since C++17, there is no need to specify the template types or use std::make_pair(), which is nice. Anyway, we then de-structure the pair into two variables x and y, change y, and print the elements in the original pair. As you might have expected, since we used auto and not auto&, it prints 0 0, i.e. modifying y does not change the original pair.

Pop quiz: If we slightly change the code and use std::tie(), what does the following code print?

int a = 0, b = 0;
auto [x, y] = std::tie(a, b);
y = 1;
std::cout << a << ' ' << b << '\n';

Perhaps surprisingly, it prints 0 1 instead of 0 0, even though we are still using a bare auto. Why? As cppreference notes, the portion of the declaration preceding [ (i.e. auto in our case) does not apply to the introduced identifiers. Instead, it applies to a hidden variable that is created by the compiler under the covers. Indeed, the structured-binding declaration

auto [x, y] = std::tie(a, b);

is roughly equivalent to

auto e = std::tie(a, b);
decltype(std::get<0>(e)) x = std::get<0>(e);
decltype(std::get<1>(e)) y = std::get<1>(e);

As you can see, auto is applied to the hidden variable e and not to the declared x and y. The type of e is std::tuple<int&, int&>, and decltype(std::get<1>(e)) gives us int&.

So, keep in mind that auto before a structured-binding declaration does not apply to the introduced identifiers, but to a compiler-defined hidden variable.

Complete Source Code

The complete source code of all examples is available on GitHub.

Discussion

Apart from comments below, you can also discuss this post at /r/cpp.

3 Comments

  1. Very nicely written and informative, thank you!

    I did not know about the second part (the interaction between auto and structured bindings). I always thought that auto was applied to the identifiers in the square brackets… Now I know.

    Reply
  2. So if I understood this correctly, in the first piece of code of the second example, the compiler will create two copies each of the values stored in the pair, first when copying the pair itself and second when destructuring? And changing auto to auto& in that code wouldn’t change the output, because the pair stores ints, not references? So there is no way to get a reference to a member of a pair of values or similar with destructuring assignment?

    Reply
    • The original code

      auto [x, y] = p;
      

      gets (roughly) translated into

      auto e = p;         // copies the original pair into a new pair
      int& x = e.first;   // x is a reference to the copy
      int& y = e.second;  // y is a reference to the copy
      

      If you wrote

      auto& [x, y] = p;
      

      the compiler would treat the code as if you (roughly) wrote

      auto& e = p;       // creates a reference to the original pair
      int& x = e.first;  // consequently, x is a reference to p.first
      int& y = e.second; // consequently, y is a reference to p.second
      

      So, changing auto to auto& would change the output.

      Reply

Leave a Comment.