c++std-rangesrange-v3

How to create a view over a key/values range?


I have a std::vector<Key> and a function std::vector<Value> lookup(const Key& key);.

I want a view over pairs of key/value as if:

auto iterate(const std::vector<Key>& keys) {
  std::vector<std::pair<Key,Value>> result;
  for (const auto& key : keys) {
    auto values = lookup(key);
    for (const auto& value : values) {
      result.emplace_back(key, value);
    }
  }
  return result;
}

for (const auto& [key, value] : iterate(keys)) {
  // do stuff
}

But I don't want to materialize this vector. I want to just iterate over it with std::ranges/views or range-v3.

Note: I'm currently stuck on GCC 11 for reasons and so owning_views are not available, in case it comes up.


Solution

  • If your function returns a std::vector<Value>, you need something to own it until you're done using it.

    If you can change your function to return a view instead, you can just do:

    auto iterate(const auto& keys)
    {
        auto transform = [&](const auto& key)
        {
            return vw::zip(vw::repeat(key), lookup(key));
        };
    
        return keys | vw::transform(transform) | vw::join;
    }
    

    and you entirely avoid the problem.

    Full example:

    #include <iostream>
    #include <map>
    #include <vector>
    
    #include <range/v3/view/all.hpp>
    #include <range/v3/view/join.hpp>
    #include <range/v3/view/repeat.hpp>
    #include <range/v3/view/transform.hpp>
    #include <range/v3/view/zip.hpp>
    
    using Key = std::string;
    using Value = std::string;
    
    namespace vw = ranges::views;
    
    std::map<Key, std::vector<Value>> map
    {
        { "Key 1", {"Value 1a", "Value 1b", "Value 1c"} },
        { "Key 2", {"Value 2a", "Value 2b", "Value 2c"} },
        { "Key 3", {"Value 3a", "Value 3b", "Value 3c"} },
    };
    
    auto lookup(Key key)
    {
        return map.at(key) | vw::all;
    }
    
    auto iterate(const auto& keys)
    {
        auto transform = [&](const auto& key)
        {
            return vw::zip(vw::repeat(key), lookup(key));
        };
    
        return keys | vw::transform(transform) | vw::join;
    }
    
    int main()
    {
        std::vector<Key> keys{"Key 1", "Key 2", "Key 3", "Key 2"};
    
        for (auto&& [key, value] : iterate(keys))
        {
            std::cout << key << " -> " << value << "\n";
        }
    }
    

    If you really can't change the lookup function, nor use a owning view, then you could call lookup for every element, like this:

    // DO NOT actually do this
    
    auto iterate(const auto& keys)
    {
        auto transform = [&](const auto& key)
        {
            return vw::zip(vw::repeat(key), vw::iota(static_cast<std::size_t>(0), lookup(key).size())
                | vw::transform([&](auto i) { return lookup(key)[i]; }));
        };
    
        return keys | vw::transform(transform) | vw::join;
    }
    

    But, of course, don't actually do this. lookup would return a new std::vector for each value.


    A better alternative would be to save the values (not thread-safe):

    // This is NOT thread-safe
    
    auto iterate(const auto& keys)
    {
        auto transform = [&](const auto& key)
        {
            // This assumes key is never empty
            static std::string heldKey;
            static std::vector<std::string> heldValue;
    
            if (heldKey != key)
            {
                heldKey = key;
                heldValue = lookup(key);
            }
    
            return vw::zip(vw::repeat(key), heldValue);
        };
    
        return keys | vw::transform(transform) | vw::join;
    }
    

    A better solution that stores the vector in the lambda passed to transform, based on @Holt's version:

    auto iterate(const auto& keys)
    {
        auto transform = [keys = std::vector<Value>{}](const auto& key) mutable
        {
            keys = lookup(key);
            return vw::zip(vw::repeat(key), keys);
        };
    
        return keys | vw::transform(transform) | vw::join;
    }
    

    And finally, for completeness, using std::views::owning_view (implicitly, GCC 12+):

    namespace vw = std::views;
    
    auto iterate(const auto& keys)
    {
        auto transform = [](const auto& key)
        {
            return lookup(key) | vw::transform([&](auto& x)
            {
                return std::make_pair(key, x);
            });
        };
    
        return keys | vw::transform(transform) | vw::join;
    }
    

    or in C++23:

    namespace vw = std::views;
    
    auto iterate(const auto& keys)
    {
        auto transform = [](const auto& key)
        {
            return vw::zip(vw::repeat(key), lookup(key));
        };
    
        return keys | vw::transform(transform) | vw::join;
    }
    

    In both of these, an std::owning_view is constructed from lookup(key) and "owns" that value until destroyed, avoiding a dangling reference.