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

3

1

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.

Aney answered 28/8, 2024 at 17:57 Comment(7)
Your example usage seems suspicious. A more sensible thing to return would be struct parsed_ints { range<int> numbers; optional<string> first_error; }, rather than a range<variant<int, string>> or whateverCarvey
@Carvey I'm not returning range<variant<int, string>> i'm actually returning a range of expected<int, error>. This is a pretty common things to do in languague like rust where ranges is the default way of doing things. But, my use case aside, this view could be useful in many scenarios in my opinon.Aney
I'm not convinced it's generally useful. You are discarding the info that most of the elements pass the predicate, and the last one might not. Callers will have to re-do a bunch of the work you've already done.Carvey
The caller just have to check if the last element is an error if i use take_while_inclusive and otherwise just transform it to a range of int. actually one of the most used function in rust is collect that does exactly that. I think an other way to understand me is asking yourself the question : how would you do that parsing using ranges?Aney
I wouldn't. It's a terminal action, resulting in a range and a possible error. Which is not the same as collect, which results in Expected<Collection<T>, E>, i.e. it discards the valid elements if one is an error.Carvey
But I want to discard it if it's an error ! ^^ (or say otherwise, collect over the range of expected ) But yeah the collecting part could be done in the try_parse function and it would returns only Expected<Collection<T>, E> that's fair :)Aney
Let us continue this discussion in chat.Aney
A
0

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]
Aney answered 1/9, 2024 at 23:33 Comment(0)
S
1

For forward_range, you can first reach the end of take_while_view, and then move forward one step to get the first iterator that does not satisfy the predicate to construct a new subrange:

auto take_while = vec | std::views::take_while(predicate);
auto last = std::ranges::next(vec.begin(), take_while.end());
auto take_while_inclusive = std::ranges::subrange(
  vec.begin(),
  std::ranges::next(last, 1, vec.end())
);

Demo

Sphygmomanometer answered 29/8, 2024 at 5:12 Comment(2)
Thank you for your reply! I had already thought of this solution but I was trying to follow the principles of views by producing the values on the fly and crossing the range only once. I forgot to mention these constraints in my question, so I'm going to edit itAney
At this point might as well use ranges::find_if_not instead of the view.Synonymous
C
1

You can ensure that it is lazily evaluated by writing it as a generator.

template<std::ranges::view V, typename Pred>
    requires ranges::input_range<V> &&
             std::indirect_unary_predicate<const Pred, std::ranges::iterator_t<V>>
std::generator<std::ranges::range_value_t<V>> take_while_inclusive(V&& view, Pred&& pred) {
    auto take_while = vec | std::views::take_while(predicate);
    auto it = take_while.begin();
    for (; it != take_while.end(); ++it) co_yield *it;
    co_yield std::ranges::elements_of(std::ranges::subrange(it, std::ranges::next(it, 1, vec.end())));
}

std::generator is in C++23, but you can backport it.

If you want it to be a range adaptor, you can use the C++23 helper std::ranges::range_adaptor_closure, or write the machinery yourself.

template<typename Pred>
requires std::is_object_v<Pred>
struct take_while_inclusive_fn : std::ranges::range_adaptor_closure<take_while_inclusive_fn> {
    Pred pred;
    template <std::ranges::view V>
    requires ranges::input_range<V> &&
        std::indirect_unary_predicate<const Pred, std::ranges::iterator_t<V>>
    auto operator()(V&& view) { return take_while_inclusive(std::forward<V>(view), pred); }
};
Carvey answered 29/8, 2024 at 8:59 Comment(2)
I hadn't thought of that! I just realized that I also forgot to specify that I was working on a constexpr parser so I'd like to see a custom view implementation but for the general case I think that's a very good answer. I'll edit my question =)Aney
If you're going to return a generator, there's no reason to use views::take_while, you can just write a loop checking the predicate. And then there's especially no reason to use elements_of, just conditionally co_yield one more element if there is one (instead of yielding the elements of a subrange that you know has at most one element in it).Synonymous
A
0

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]
Aney answered 1/9, 2024 at 23:33 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.