0

The original question

I have a piece of code where I process a std::vector by splitting it into groups and processing each group.

An if-else determines the processing each group undergoes:

  • groups taking the if branch will end up giving a single ouptut,
  • groups taking the else branch give rise to several outputs.

All these outputs are collected in a vector.

This is a simplification of the code:

#include <range/v3/view/group_by.hpp>
#include <vector>

namespace rv = ranges::views;

auto f1 = [](auto){ return 7; /* this actually does depend on the input */ };
auto f2 = [](auto){ return 9; /* this actually does depend on the input */ };
auto f3 = [](auto){ return 5; /* this actually does depend on the input */ };
auto f4 = [](auto){ return 8; /* this actually does depend on the input */ };

int main() {
    // input
    std::vector<int> v{1, 1, 1, 0, 0, 3, 3, 2, 2, 2};

    // prepare output
    std::vector<int> w; // and resize appropriately

    // populate w in a loop
    for (auto g : v | rv::group_by(std::equal_to<>{})) {
        if (g.front() != 3) {
            // for these groups only one item is stored into w
            w.push_back(f1(g));
        } else {
            // for these groups several items are stored into w
            w.push_back(f2(g));
            w.push_back(f3(g));
            w.push_back(f4(g));
        }
    }
}

I can't help but think that, after all, I'm just doing monadic binding, i.e.

  • transforming the output of group_by, group by group,
  • where each group can result in
    • a sequence of several elements (else branch)
    • a single element (if branch), which can be tought of as a one-element sequence
  • joining the sequences

Therefore, I'd be tempted to simplify the code as follows:

#include <range/v3/view/join.hpp>
#include <range/v3/view/transform.hpp>
#include <range/v3/view/group_by.hpp>
#include <vector>

namespace rv = ranges::views;

auto f = [](auto x){
    auto one = std::vector<int>{7};
    auto many = std::vector<int>{9,5,8};
    return (x.front() != 3) ? one
                            : many;
};

int main() {

    std::vector<int> v{1, 1, 1, 0, 0, 3, 3, 2, 2, 2};

    auto w = v | rv::group_by(std::equal_to<>{})
               | rv::transform(f)
               | rv::join;
}

This code, however, doesn't work because f is not returning views for each group, but rather true std::vectors.

In some way, I think, I would need to wrap the single 7 and the 9,5,8 into a range, so that join can make its job.

How can I do that?

Update after some time

The fact of the matter is that I'm hitting The Surprising Limitations of C++ Ranges Beyond Trivial Cases.

However, in that article there's a link to this answer here on SO which is indeed an answer to my question too, which consists in changing this

    auto w = v | rv::group_by(std::equal_to<>{})
               | rv::transform(f)
               | rv::join;

to this

    auto w = v | rv::group_by(std::equal_to<>{})
               | rv::transform(f)
               | rv::cache1
               | rv::join;

where cache1 caches the results of transform thus preventing join from storing dangling references after the temporary std::vectors are gone.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • If the two vectors were declared const in the global scope, returning a range is trivial. – Michaël Roy Feb 19 '21 at 20:49
  • @MichaëlRoy, no, they are not. They are computed based on the inputs. – Enlico Feb 19 '21 at 21:05
  • I'm trying to understand your code but have no idea what the `|` operator does with a vector, and so far I have not been able to find relevant documentation. – vacuumhead Feb 19 '21 at 21:28
  • If you refer to the `|` in front of `rv::join`, it doesn't work, and that's the reason the code doesn't compile. If you refer to `|` in general, that's the pipe operator; search for _Range adaptors_ [here](https://en.cppreference.com/w/cpp/ranges). – Enlico Feb 19 '21 at 21:35

0 Answers0