Search code examples
c++tuplesc++17autostructured-bindings

Ranges V3 zip with auto and structured bindings


I would like to be able to use C++ ranges to help simplify code logic via zipping containers rather than explicitly indexing into them. I can get this to work with a verbose lambda argument, but I would rather try to make it simpler/generalizable with more auto.

const int n = ...;
std::vector<float> a(n), b(n), c(n);

...initialize a and b...

// This works
ranges::for_each(
    ranges::views::zip(a, b, c),
    [](const std::tuple<float&, float&, float&>& v)
    {
        const auto& [a, b, c] = v;
        c = a + b; 
        std::cout << typeid(v).name(); // NSt3__15tupleIJRfS1_S1_EEE
    }
);

// This fails
ranges::for_each(
    ranges::views::zip(a, b, c),
    [](const auto& v)
    {
        const auto& [a, b, c] = v;
        // c = a + b; 
        std::cout << typeid(v).name(); // N6ranges12common_tupleIJRfS1_S1_EEE
    }
);

The Ranges-v3 documentation says the following:

views::zip

Given N ranges, return a new range where Mth element is the result of calling make_tuple on the Mth elements of all N ranges.

This makes me think that I should be able to convert the ranges::common_tuple into a std::tuple, and I looked at the public members and found:

std::tuple< Ts... > const & base() const noexcept

However this doesn't compile either:

const auto& [a, b, c] = v.base();
// error: no member named 'base' in 'std::__1::tuple<float, float, float>'

But when I print of the typeid(v) it is not std::tuple; it is ranges::common_tuple. Is what I'm trying to do here with auto type deduction possible? (clang compiler if that matters)


Solution

  • The short answer is: don't use const if you don't actually need const. You want to modify something, so why const? This works fine:

    ranges::for_each(
        ranges::views::zip(a, b, c),
        [](auto&& v)
        {
            auto&& [a, b, c] = v;
            c = a + b; 
        }
    );
    

    As does the shorter:

    for (auto&& [a, b, c] : ranges::views::zip(a, b, c)) {
        c = a + b;
    }
    

    The reason what you have breaks is kind of subtle. Basically, ranges::for_each is constrained on indirectly_unary_invocable, which is requires all of:

            invocable<F &, iter_value_t<I> &> &&
            invocable<F &, iter_reference_t<I>> &&
            invocable<F &, iter_common_reference_t<I>> &&
    

    So your lambda gets instantiated with all three of those types. One of those types (iter_value_t<I>&) is tuple<float, float, float>&. So when you do the structured binding with const auto&, the type of each of the bindings is const float. That's why it's not assignable - but that's only true for that specific instantiation (which isn't the one that gets invoked at runtime anyway).