1

So let's say you want to make your own custom C++20 ranges-alike view. Maybe my::views::enumerate. There are a bunch of examples on the internet how to make the view itself, fulfilling all the requirements and how to make the adaptor object. But here is the catch: standard range adaptors allow you to "pre-combine them", say

auto make_odd_cookie = std::views::filter{is_odd} | std::views::transform{make_cookie};

for (auto cookie : sources | make_odd_cookie) ...

Now, if you add the my::ranges::enumerate to the mix, implemented according to all the guidelines

// That works
for (auto [i, cookie] : sources | make_odd_cookie | my::views::enumerate) ...

auto make_odd_enumerated_cookie = make_odd_cookie | my::views::enumerate; // that does not

the pre-combination of standard and custom adaptors fails. Even if you'll copy-paste the your-standard-library-of-choice implementation of the adaptor, you'll only be able to pre-combine custom adaptors with custom adaptors and standard with standard, but not mix them

// say copy-pasted from libstdc++ implementation of _RangeAdaptorClosure
struct my::ranges::adaptor_closure {
    friend auto operator|(const adaptor_closure& a, const adaptor_closure& b) {
       ...
    }
};

auto indexed_tokens = my::views::tokenize{"\n"} | my::views::enumerate; // works

auto indexed_add_tokens =
    my::views::tokenize{"\n"}
    | std::views::filter{is_add}
    | my::ranges::enumerate; // does not

Reason for this behaviour is the fact that the standard range adaptor has undefined type, so you can't really define an operator| that will accept both custom and standard adaptor types (there theoretically might not even be a standard adaptor type, although I am not sure how to implement the pre-combination feature then)

struct my::ranges::adaptor_closure {
    friend auto operator|(const std::ranges::adaptor_type_does_not_exist<???>& a, const adaptor_closure& b) {
       ...
    }
};

At the same time it would be nice to have ability to define your own custom range adaptors that would behave exactly the same way as the standard ones, including the ability to pre-combine with any other range adaptor, including standard and third-party ones

Currently, I see two options (besides introducing some sort of std::ranges::enable_range_adaptor = true trait into the standard):

  1. Accept any type as the second adaptor for the merging operator|: operator|(auto& a, const adaptor_closure& b) (and other way around) and just ignore the possible misuse
  2. Add some sort of a concept is_range_adaptor = std::is_callable<T, std::span<int>> && has_pipe_operator<std::span<int>, T> or similar, with only catch being using some sort of fake range and hoping that it's generic enough to fit any sort of a range adaptor

First seems to be the "proper" way, but it's sort of just giving up and admitting there is no clean way to do it. Second sounds like a hack because what if you have a range adaptor (possibly a third-party one) that expects a somewhat special range - this will fail

Any better ideas how to achieve cross-adaptor compatibility? Maybe I just missed some tricky concept within the standard that allows to check if type is an adaptor?

cigien
  • 57,834
  • 11
  • 73
  • 112
Andrian Nord
  • 677
  • 4
  • 11

0 Answers0