c++c++20std-ranges

Replacement for std::views::zip/keys in C++20


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:

  1. I can overload function foo with one which takes masks array to and process there, but I don’t want to have code duplication, since real function foo is complex.
  2. I can pass additional parameter to function foo which would take a pointer to masks array and default it to 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?


Solution

  • If all you need is to zip two vector<float> to combine the contents, the simplest thing you can do is:

    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]; })
        ;