I'm replacing our own in-house ZipIterator
/ZipRange
implementation with std::views::zip
and am running into a wall when it comes to TBB's parallel_for_each
.
With our own ZipRange, it was perfectly capable of modifying one of the elements returned by the ZipRange, but when I replace it with std::views::zip, the functor seems to get a copy, and not a modifiable reference as I would expect, resulting in the parallel_for_each having to effect and leaving the "result" container unmodified.
Since it worked with our (very euhm, involved) in-house ZipIterator/ZipRange, and std::ranges::for_each does what it's supposed to do, but tbb::parallel_for_each leaves the elements unchanged, as demonstrated with this program:
#include <ranges>
#include <vector>
#include <iostream>
#include <algorithm>
#include <tbb/parallel_for_each.h>
int main()
{
const std::vector<int> ints{1,2,3};
const std::vector<double> doubles{0.1, 0.2, 0.3};
const auto multiply = [](auto&& intDoubleProduct)
{
auto&& [i, d, product] = intDoubleProduct;
product = i * d;
};
{
std::vector<double> product = {0, 0, 0};
std::ranges::for_each(std::views::zip(ints, doubles, product), multiply);
std::cout << "std::ranges::for_each result: (";
for(auto value : product)
std::cout << value << ", ";
std::cout << '\n';
}
{
std::vector<double> product = {0, 0, 0};
tbb::parallel_for_each(std::views::zip(ints, doubles, product), multiply);
std::cout << "tbb::parallel_for_each result: (";
for(auto value : product)
std::cout << value << ", ";
std::cout << '\n';
}
}
How can I make a read-write std::views::zip
work with tbb::parallel_for_each
?
The only relevant reference I could discover is what seems like an admission of a bug in the TBB code, but I would think that has long been fixed (I cannot find the referenced code any more, so maybe this case was lost in some refactoring on their end?):
I'm replacing our own in-house ZipIterator
/ZipRange
implementation with std::views::zip
and am running into a wall when it comes to TBB's parallel_for_each
.
With our own ZipRange, it was perfectly capable of modifying one of the elements returned by the ZipRange, but when I replace it with std::views::zip, the functor seems to get a copy, and not a modifiable reference as I would expect, resulting in the parallel_for_each having to effect and leaving the "result" container unmodified.
Since it worked with our (very euhm, involved) in-house ZipIterator/ZipRange, and std::ranges::for_each does what it's supposed to do, but tbb::parallel_for_each leaves the elements unchanged, as demonstrated with this program:
#include <ranges>
#include <vector>
#include <iostream>
#include <algorithm>
#include <tbb/parallel_for_each.h>
int main()
{
const std::vector<int> ints{1,2,3};
const std::vector<double> doubles{0.1, 0.2, 0.3};
const auto multiply = [](auto&& intDoubleProduct)
{
auto&& [i, d, product] = intDoubleProduct;
product = i * d;
};
{
std::vector<double> product = {0, 0, 0};
std::ranges::for_each(std::views::zip(ints, doubles, product), multiply);
std::cout << "std::ranges::for_each result: (";
for(auto value : product)
std::cout << value << ", ";
std::cout << '\n';
}
{
std::vector<double> product = {0, 0, 0};
tbb::parallel_for_each(std::views::zip(ints, doubles, product), multiply);
std::cout << "tbb::parallel_for_each result: (";
for(auto value : product)
std::cout << value << ", ";
std::cout << '\n';
}
}
https://godbolt./z/EvMWGsTP5
How can I make a read-write std::views::zip
work with tbb::parallel_for_each
?
The only relevant reference I could discover is what seems like an admission of a bug in the TBB code, but I would think that has long been fixed (I cannot find the referenced code any more, so maybe this case was lost in some refactoring on their end?): https://community.intel/t5/Intel-oneAPI-Threading-Building/tbb-parallel-for-and-std-for-each/m-p/848767
2 Answers
Reset to default 0The issue is that std::ranges::zip_view<Rs...>::value_type
is std::tuple<std::ranges::range_value_t<Rs>...>
. You need to turn it into a range of std::tuple<std::ranges::range_reference_t<Rs>...>
instead.
You can do that by zip_transform
ing with std::forward_as_tuple
wrapped in a lambda (as it isn't an addressable function).
See Godbolt
std::ranges::for_each
doesn't use the value_type
of the passed in range, it does1 f(proj(*it))
.
- something not observably different to
I'm not sure about this, but TBB’s parallel_for_each doesn’t “see through” the proxy that std::views::zip
produces – it ends up passing a copy of the tuple, so modifying that copy doesn’t update your original container. One proven workaround is to “materialize” the underlying references by piping the zip view through std::views::all
. For example, change your call to:
tbb::parallel_for_each(
std::views::zip(ints, doubles, product) | std::views::all,
multiply
);
This issue is due to how TBB’s algorithm deduces and passes its elements (requiring forward iterator semantics), while std::views::zip
returns proxy objects that don’t quite meet that requirement. Piping with std::views::all
“unwraps” the proxies into proper references.