5

I saw the code below here. The description says:

We can also capture as a forwarding reference using auto&&. That is, auto&& will resolve to auto& for lvalue references, and auto&& for rvalud references. Here is an example of capturing the output from a range-based for loop over a temporary map.

I do understand the decaying of auto&& to auto& if the value referred to is an lvalue (so in this case there is no decay). What I struggling with, is to understand how the temporary map, the range based for loop and the moved values work together. Would you care to explain how these things work with each other and why it's ok to move from a temporary that is being iterated.

#include <iostream>
#include <map>

std::map<std::string, int> get_map()
{
    return {
        { "hello", 1 },
        { "world", 2 },
        { "it's",  3 },
        { "me",    4 },
    };
}

int main() {
    for (auto&& [ k, v ] : get_map())
        std::cout << "k=" << k << " v=" << v << '\n';
}
dani
  • 3,677
  • 4
  • 26
  • 60
  • 3
    For the sake of brevity, you already understand [reference collapsing](https://stackoverflow.com/questions/13725747/concise-explanation-of-reference-collapsing-rules-requested-1-a-a-2). Is that correct ? – WhozCraig Aug 08 '17 at 19:45
  • fyi: no moving going on here, just potentially RVO in the function return – Richard Critten Aug 08 '17 at 20:15
  • @RichardCritten Since this is tagged [c++1z], there is no “potential RVO”. That’s a temporary and definitely won’t materialize until it’s needed by the for loop. Before C++17 it would be implementation-defined, but here it’s required and is no longer quite the same thing as RVO. – Daniel H Aug 08 '17 at 20:52

1 Answers1

1

There are two different uses of auto&& in this example, one visible, one hidden. To be maximally verbose, the loop you have expands out to:

{
    auto&& __range = get_map(); // the temporary get_map() is bound to a reference, which 
                                // extends its lifetime to the lifetime of the reference
                                // decltype(__range) is std::map<std::string, int>&&

    auto __begin = __range.begin();
    auto __end = __range.end();     // potential different type from __begin since C++17
    for (; __begin != __end; ++__begin) {
        auto&& __elem = *__begin;  // this is your structured binding declaration
                                   // *__begin is an lvalue, so decltype(__elem) is
                                   // std::pair<std::string const, int> const&


        // the actual destructuring here
        std::string const& k = std::get<0>(__elem);    
        int const& v         = std::get<1>(__elem);

        // now your body
        std::cout << "k=" << k << " v=" << v << '\n';
    }
}

Hence:

why it's ok to move from a temporary that is being iterated.

There is no moving happening anywhere in this code. The map is constructed in place in __range, and that's what you're iterating over. It goes out of scope in the last brace.


Note that this:

I do understand the decaying of auto&& to auto& if the value referred to is an lvalue

isn't quite correct. First, it's not called "decay" - decay is what happens when, for instance, you pass an array into a function - it decays into a pointer. Moreover, auto&& doesn't collapse into auto&. It's just that, if the initializer is an lvalue, auto&& and auto& behave the same way. If the initializer is an rvalue, auto&& works (and yields an rvalue reference) whereas auto& fails to compile.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    Your last part isn’t quite correct either. If your initializer is an lvalue, `auto&&` deduces `auto` as `auto&` and *does* do reference collapse. You can see all the details in [this post by Scott Meyers](https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers). – Daniel H Aug 08 '17 at 20:49