Search code examples
c++c++20std-rangesrange-v3

Implementing take_while_inclusive (or std::delimit): views::take_while but including element missing predicate


I was working on a simple parser using std::ranges. I was trying to parse ints in a string until all were converted or one failed by doing something like:

try_parse_ints(str) | take_while(is valid int)

But in case of an error, I wanted to obtain the last result corresponding to the error so that I could return it to the user.

So I thought I'd like to have a views::take_while_inclusive that would stop only after returning the first element not respecting the predicate.

I tried to implement this using existing std::views and came up with a dirty trick using views::take_while and a mutable hidden in the predicate like this:

constexpr auto take_while_inclusive(auto&& predicate) {
    const auto custom_predicate =
        [predicate =
             std::forward<decltype(predicate)>(predicate)](auto&& value) {
            static bool found = true;
            return std::exchange(found, predicate(value));
        };
    return std::views::take_while(custom_predicate);
}

Demo

I know that the take_while predicate should be const but I don't know how else to do it and I have no idea how to implement it using a custom view.

Could you help me implement it properly?

EDIT 1

I forgot to mention it in the first version of this question but I'm looking for a solution acting like a standard std::views by generating the range on the fly and iterate to the input range only once.

EDIT 2

Although @Caleth's answer is excellent in general cases, I also forgot to mention that I'd like to be able to use it at compile time.


Solution

  • Answer to my inititial question

    I found out that an actual implementation of what I called take_while_inclusive in my initial question has been proposed for c++26 in P3220R0 under the name std::delimit (which is actually a much better name, in my opinion)

    An implementation for libc++ can be found here

    If you don't have the same constraints (constexpr, lazy, pass only once) as I do, I think the other answers suggested here are fine, but this solution brings together the best of all worlds.


    However

    As is often the case when we try to find the solution to a problem, we end up looking for the solution to a sub-problem without realizing that we're trying to solve the question from the wrong end. This was the case for me here and I thank @Caleth for pointing it out to me.

    If like me what you areally attempting to do is to collect a range of expected into an expected of range I think you should try to use collect

    Basic behavior :

    #include <fmt/ranges.h>
    int main() {
        std::vector<std::expected<int, std::string>> has_error = {
            1, 2, std::unexpected("NOT INT")};
        std::vector<std::expected<int, std::string>> no_error = {1, 2, 3};
    
        std::expected<std::vector<int>, std::string> exp_error = has_error 
            | views::collect();
        auto exp_value = no_error | views::collect();
    
        auto print = [](const auto& expected) {
            if (expected.has_value())
                fmt::println("Valid result : {}", expected.value());
            else
                fmt::println("Error : {}", expected.error());
        };
    
        print(exp_error);
        print(exp_value);
    }
    

    Output :

    Error : NOT INT
    Valid result : [1, 2, 3]