Search code examples
c++c++20std-ranges

Basic use of std::range similar to iota


I’m trying to use std ranges for the first time. I have a few arrays, I use them to generate std::vector of std::string_view’s:

static std::vector<std::string_view> x;

const char data[] = "1231234567812345";
const int offsets[] = { 0, 3, 11 };
const int sizes[] = { 3, 8, 5 };

const std::vector<std::string_view>& getRange()
{
    if (x.empty())
    {
        x.resize(sizeof(sizes) / sizeof(sizes[0]));
        for (size_t i = 0; i < x.size(); ++i)
            x[i] = std::string_view(data + offsets[i], data + offsets[i] + sizes[i]);
    }
    return x;
}

Users of the getRange simply want to be able to iterate elements, get size of the sequence and to access elements using operator[]. As I understand, this code could be a good candidate to change the code to use ranges, so that instead of generating vector of string views, I could use range that generates the string views lazily when requested (similarly to ranges::iota). What should I use to generate such range from these arrays without creating intermediary vector?

Update:

This seems to work:

auto sv = [](int i) { return std::string_view(data + offsets[i], data + offsets[i] + sizes[i]); };
auto rangeView = std::views::iota(0u, sizeof(sizes) / sizeof(sizes[0])) | std::views::transform(sv);

rangeView is now functionally equivalent to getRange. See the code on coliru.

In my case getRange is declared in .h, and defined in .cpp file. Can do the same with rangeView?


Solution

  • C++23 has std::views::zip_transform for this:

    auto getRange() {
        return std::views::zip_transform(
            [](int offset, int size) {
                return std::string_view(data + offset, data + offset + size);
            },
            offsets, sizes);
    }
    

    In C++20, the same can be achieved using std::views::iota to generate input range for std::views::transform:

    auto getRangeOld() {
        return std::views::iota(std::size_t{}, std::size(sizes)) |
               std::views::transform([](std::size_t i) {
                   return std::string_view(data + offsets[i],
                                           data + offsets[i] + sizes[i]);
               });
    }
    

    See it online!

    Minor caveat: if you ever allow the ranges to be of different size, getRange automatically gets minimal size of all the ranges. getRangeOld will have UB if sizes is larger than offsets, though the remedy is simple std::min.

    If data can be std::string_view, you can use dataStringView.substr() method instead to get substrings more safely - if offsets[i] + sizes[i] is greater than std::size(data), you'd have UB. dataStringView.substr(offsets[i], sizes[i]) is both easier to read and safe from the UB.

    And final note: I don't know if this is actually more performant than your current implementation. Ranges are somewhat new addition and they may or may be well optimised by the compiler. Test and check if the ranges version is actually better in your environment or not. Now I come to think of this, you can also make it a global constexpr object of view type instead of generating it on the fly - may be a little bit easier without the extra function around it.