Search code examples
c++lambdac++20perfect-forwarding

Perfect forwarding of pair.second using a lambda


Why this snippet doesn't compile?

#include <iostream>
#include <vector>
#include <ranges>
#include <unordered_map>

namespace vw = std::ranges::views;

int main()
{
    auto get_second = [](auto&& pair) constexpr noexcept -> decltype(auto)
                      { return std::forward<decltype(pair)>(pair).second; };
    
    std::unordered_map<unsigned, std::pair<double, char> > m = {{5, {0., 'a'}}};
    

    for (auto& [d, c] : m | vw::transform(get_second))
        c = 'b';

    for (auto const& pair : m)
        std::printf("(%u, (%.3f, %c))\n", pair.first, pair.second.first, pair.second.second);
}

The error, using gcc is:

main.cpp: In function 'int main()':
main.cpp:16:53: error: cannot bind non-const lvalue reference of type 'std::pair<double, char>&' to an rvalue of type 'std::__success_type<std::pair<double, char> >::type' {aka 'std::pair<double, char>'}
   16 |     for (auto& [d, c] : m | vw::transform(get_second))
      |                                                     ^

Shouldn't -> decltype(auto) resolve to std::pair<double, char>&? If I replace -> decltype(auto) by -> std::pair<double, char>& it works as expected.


Solution

  • Shouldn't -> decltype(auto) evaluate to std::pair<double, char>&?

    No. Here's a much simpler example:

    struct X {
        int i;
    };
    
    X x{42};
    decltype(auto) i = x.i;
    

    Is i an int or an int&? It's an int. decltype(auto) derives its type by applying decltype(...) to the right-hand side. decltype(x.i) just gives you the type of the member, that's int.

    In order to get an int& you have to do:

    decltype(auto) i = (x.i);
    

    Because now we get the type as decltype((x.i)), which yields int&.

    decltype has a special rule for unparenthesized access - so adding parentheses sidesteps it. This is why decltype(x.i) and decltype((x.i)) can differ. Once we sidestep that one, decltype on an lvalue of type T yields the type T&. x.i is an lvalue of type int, so we get int&.

    Note that I said can differ and not must differ, if the member i were of type int&, then both decltype(x.i) and decltype((x.i)) would be int&.


    Going back to the original example, you have the choice of doing either parenthesizing the returned expression (and dropping the unnecessary constexpr):

    auto get_second = [](auto&& pair) noexcept -> decltype(auto)
                      { return (FWD(pair).second); };
    

    Or just knowing that because we're doing class member access, this will never be a prvalue, so we can simplify to using auto&& (without the need for additional parentheses):

    auto get_second = [](auto&& pair) noexcept -> auto&&
                      { return FWD(pair).second; };
    

    Also the standard library itself comes with shorthands for this:

    for (auto& [d, c] : m | vw::transform(get_second))
    

    You can instead write:

    for (auto& [d, c] : m | vw::values)
    

    (or also elements<1>, in case you need other elements).


    Lastly, the typical choice for a short name for the views namespace is rv (rather than vw). Or just use views.