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
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 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] << '\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.
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 thatauto
was applied to the identifiers in the square brackets… Now I know.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?
The original code
gets (roughly) translated into
If you wrote
the compiler would treat the code as if you (roughly) wrote
So, changing
auto
toauto&
would change the output.