14

When running a range-based for loop on an std::unordered_map it appears that the type of the loop variable does not use reference types:

std::unordered_map<int, int> map = { {0, 1}, {1, 2}, {2, 3} };
for(auto&[l, r] : map)
    static_assert(std::is_same_v<decltype(r), int&>);

MSVC 2017, gcc 8.2 and clang 7.0.0 all report a failed assertion here. Oppose this to a std::vector, where the assertion does not fail, as one would expect:

std::vector<int> vec = { 1, 2, 3 };
for(auto& r : vec)
    static_assert(std::is_same_v<decltype(r), int&>);

However on both MSVC 2017 and gcc 8.2 a loop modifying the local variable r will have observable side-effects:

#include <iostream>
#include <type_traits>
#include <unordered_map>
#include <vector>

int main() {
    std::unordered_map<int, int> a = { {0, 1}, {1, 2}, {2, 3} };
    for(auto[l, r] : a)
        std::cout << l << "; " << r << std::endl;
    for(auto&[l, r] : a) {
        static_assert(std::is_same_v<decltype(r), int>);
        r++;
    }
    std::cout << "Increment:" << std::endl;
    for(auto[l, r] : a)
        std::cout << l << "; " << r << std::endl;
}

This program for example will print (ignoring order):

0; 1
1; 2
2; 3
Increment:
0; 2
1; 3
2; 4

What am I missing? How can this change the value in the map despite the local variable not being of reference type? Or probably more appropriately, why does std::is_same not see the right type, because quite clearly it is a reference type? Or am I alternatively missing some undefined behavior?

Note that I did reproduce the same issue without using structured bindings, so I keep the nice looking code here. See here for an example

Swordfish
  • 12,971
  • 3
  • 21
  • 43
Hanno Bänsch
  • 284
  • 3
  • 13
  • 3
    The culprit is `decltype`: [*"If the argument is an unparenthesized id-expression naming a structured binding, then decltype yields the referenced type"*](https://en.cppreference.com/w/cpp/language/decltype). But I admit I have no clue what's the rationale for that. – HolyBlackCat Oct 21 '18 at 11:10
  • 1
    @HolyBlackCat to make C++ even more confusing and less consistent. Seems to be the rationale for everything we added in the last years. – Stephan Dollberg Oct 21 '18 at 11:21
  • 3
    [What are the types of identifiers introduced by structured bindings in C++17?](https://stackoverflow.com/questions/44671697/what-are-the-types-of-identifiers-introduced-by-structured-bindings-in-c17); [structured bindings: when something looks like a reference and behaves similarly to a reference, but it's not a reference](https://stackoverflow.com/questions/44695684/structured-bindings-when-something-looks-like-a-reference-and-behaves-similarly) – cpplearner Oct 21 '18 at 11:35

1 Answers1

16

Structured bindings are modeled as aliases, not "real" references. Even though they may use a reference under the hood.

Imagine that you have

struct X {
    const int first = 0;
    int second;
    int third : 8;
};

X x;
X& y = x;

What's decltype(x.second)? int. What's decltype(y.second)? int. And so in

auto& [first, second, third] = x;

decltype(second) is int, because second is an alias for x.second. And third poses no problems even though it's not allowed to bind a reference to a bit-field, because it's an alias, not an actual reference.

The tuple-like case is designed to be consistent with that. Even though in that case the language has to use references, it does its best to pretend that those references do not exist.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • But this is something that only the compiler is allowed to do? As a user I can never define aliases to variables, right? – Hanno Bänsch Oct 21 '18 at 12:10
  • Missing here is pointing out what the `&` in the `auto&` does, if not qualifying the introduced names as the OP expected: it qualifies the invisible object of type `decltype(x)`, to whose members those names refer. There is effectively an invisible reference of that type, and the new names refer to members therein, with the result that updating them is updating `x` too. – underscore_d Oct 21 '18 at 15:21