I need to port some code from C++ 23 which uses std::views::zip
/keys
to C++ 20 (porting to CUDA) (which doesn’t have std::views::zip
) having only one specific use case of this code.
I have data in one std::vector
and masks for data in another std::vector
. Some function works with a range and updates all elements of data vector and I want to use it for the values/masks pairs. So, this function should process only those values which have 1.0f
in the respective masks.
The code in on the critical path and I don’t want to get intermediate vectors allocations (like of copy of filtered values), so I rely on ranges/views lazy evaluation hard here. I expect that the generated code will deal with the original vectors without any temporary copies.
With C++ 23 it is implemented via std::views::zip
and std::views::keys
, see the demo (function foo
is a simplified dummy version for a sake of example, of course):
#include <algorithm>
#include <ranges>
#include <vector>
#include <iostream>
void foo(std::ranges::input_range auto& data) {
for (auto it = data.begin(); it != data.end(); ++it) {
*it = 7.0f;
}
}
int main()
{
std::vector<float> values = { 0.0f, 1.0f, 0.0f, 2.0f };
std::vector<float> masks = { 0.0f, 1.0f, 1.0f, 0.0f };
auto filtered = std::views::zip(values, masks) |
std::views::filter([](const auto& tuple) { return std::get<1>(tuple) != 0.0f; });
auto masked = std::views::keys(filtered);
foo(masked);
std::ranges::copy
(
values,
std::ostream_iterator<float>{std::cout, ", "}
);
std::cout << std::endl;
}
With C++ 20 I have some options here. I implemented this version the demo
#include <algorithm>
#include <ranges>
#include <vector>
#include <iostream>
void foo(std::ranges::input_range auto& data) {
for (auto it = data.begin(); it != data.end(); ++it) {
*it = 7.0f;
}
}
int main()
{
std::vector<float> values = { 0.0f, 1.0f, 0.0f, 2.0f };
std::vector<float> masks = { 0.0f, 1.0f, 1.0f, 0.0f };
auto masked = values | std::views::filter([&](const auto& value) {
return masks[&value - values.data()];
});
foo(masked);
std::ranges::copy
(
values,
std::ostream_iterator<float>{std::cout, ", "}
);
std::cout << std::endl;
}
which works just fine, but seems to be a “hack” with this getting index by subtracting values.data()
address from address of the value
; well, this is not the worst thing, but could be fragile and could be in the way of compiler to implement lazy ranges evaluation, so I consider it “good enough”, but still not sure that this is the best way to do so.
I have some other options, which I still don’t like:
nullptr
checking later in foo, but it already have some default parameters, so this would be inconvenient.I feel that I could invent something with std::ranges::views::elements
/ std::ranges::views::keys
/ std::ranges::views::values
, but can’t see the way.
What would be the bet way to implement this in C++ 20 here?
If all you need is to zip two vector<float>
to combine the contents, the simplest thing you can do is:
views::iota
to count from 0
to the minimum size of the vectors
transform
that index into a tuple
(or pair
or named struct or whatever).For instance:
std::vector<float> values = { 0.0f, 1.0f, 0.0f, 2.0f };
std::vector<float> masks = { 0.0f, 1.0f, 1.0f, 0.0f };
auto nonzero_values =
views::iota(size_t(), std::min(values.size(), masks.size()))
| views::transform([&](size_t i){
struct V {
float& value;
float mask;
};
return V { .value=values[i], .mask=masks[i] };
})
| views::filter([](auto v){ return v.mask != 0.0f; })
| views::transform([](auto v) -> float& { return v.value; })
;
Or, simpler:
auto nonzero_values =
views::iota(size_t(), std::min(values.size(), masks.size()))
| views::filter([&](size_t i){ return masks[i] != 0.0f; })
| views::transform([&](size_t i) -> float& { return values[i]; })
;