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)
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).