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 << '

'; // 0 1

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

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

'; // 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] << '

';

We create a vector of int s 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] << '

';

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 << '

';

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 << '

';

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.