Реализация take_ while_inclusive :views::take_ while, но с включением предиката отсутствия элемента

Я работал над простым парсером, используя std::ranges. Я пытался анализировать целые числа в строке до тех пор, пока все они не были преобразованы или один не потерпел неудачу, выполнив что-то вроде:

try_parse_ints(str) | take_while(is valid int)

Но в случае ошибки мне хотелось получить последний результат, соответствующий ошибке, чтобы можно было вернуть его пользователю.

Поэтому я подумал, что мне бы хотелось иметь представление::take_ while_inclusive, которое останавливалось бы только после возврата первого элемента, не соблюдающего предикат.

Я попытался реализовать это, используя существующий std::views, и придумал грязный трюк, используя view::take_ while и изменяемый объект, скрытый в предикате, например:

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);
}

Демо

Я знаю, что предикат take_ while должен быть константным, но я не знаю, как еще это сделать, и понятия не имею, как реализовать его с использованием специального представления.

Не могли бы вы помочь мне реализовать это правильно?

РЕДАКТИРОВАТЬ 1

Я забыл упомянуть об этом в первой версии этого вопроса, но я ищу решение, действующее как стандартное std::views, генерируя диапазон на лету и повторяя входной диапазон только один раз.

РЕДАКТИРОВАТЬ 2

Хотя ответ @Caleth превосходен в общих случаях, я также забыл упомянуть, что хотел бы иметь возможность использовать его во время компиляции.

Использование вашего примера кажется подозрительным. Более разумно было бы вернуть struct parsed_ints { range<int> numbers; optional<string> first_error; }, а не range<variant<int, string>> или что-то в этом роде.

Caleth 29.08.2024 10:25

@Калет Я не возвращаюсь range<variant<int, string>>, на самом деле я возвращаю диапазон expected<int, error>. Это довольно распространенная вещь в языке, таком как ржавчина, где диапазоны являются способом выполнения действий по умолчанию. Но, если оставить в стороне мой вариант использования, по моему мнению, это представление может быть полезно во многих сценариях.

fourmisnuee 29.08.2024 10:34

Я не уверен, что это вообще полезно. Вы отбрасываете информацию о том, что большинство элементов передают предикат, а последний — нет. Звонящим придется заново проделать большую часть работы, которую вы уже проделали.

Caleth 29.08.2024 10:40

Вызывающий просто должен проверить, является ли последний элемент ошибкой, если я использую take_ while_inclusive, а в противном случае просто преобразовать его в диапазон int. на самом деле одна из наиболее часто используемых функций в Rust — это сбор, которая делает именно именно это. Я думаю, что другой способ понять меня — задать себе вопрос: как бы вы выполнили этот анализ, используя диапазоны?

fourmisnuee 29.08.2024 10:47

Я бы не стал. Это завершающее действие, приводящее к диапазону и возможной ошибке. Это не то же самое, что collect, которое приводит к Expected<Collection<T>, E>, т. е. отбрасывает допустимые элементы, если один из них является ошибкой.

Caleth 29.08.2024 11:02

Но я хочу отказаться от него, если это ошибка! ^^ (или, скажем иначе, собирать в пределах ожидаемого диапазона). Но да, часть сбора можно выполнить в функции try_parse, и она вернет только Expected<Collection<T>, E>, это справедливо :)

fourmisnuee 29.08.2024 11:09

Давайте продолжим обсуждение в чате.

fourmisnuee 29.08.2024 11:14
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
98
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Для forward_range вы можете сначала дойти до конца take_while_view, а затем продвинуться на один шаг вперед, чтобы получить первый итератор, который не удовлетворяет предикату для создания нового 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())
);

Демо

Спасибо за ваш ответ! Я уже думал об этом решении, но пытался следовать принципам представлений, создавая значения на лету и пересекая диапазон только один раз. Я забыл упомянуть эти ограничения в своем вопросе, поэтому собираюсь его отредактировать.

fourmisnuee 29.08.2024 10:03

На этом этапе можно было бы использовать ranges::find_if_not вместо представления.

Barry 29.08.2024 15:50

Вы можете гарантировать, что он будет лениво оцениваться, написав его как генератор.

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 находится на C++23, но вы можете его бэкпортировать.

Если вы хотите, чтобы это был адаптер диапазона, вы можете использовать помощник C++23 std::ranges::range_adaptor_closure или написать механизм самостоятельно.

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); }
};

Я об этом не подумал! Я только что понял, что забыл указать, что работаю над синтаксическим анализатором constexpr, поэтому мне хотелось бы увидеть реализацию собственного представления, но в общем случае я думаю, что это очень хороший ответ. Я отредактирую свой вопрос =)

fourmisnuee 29.08.2024 11:04

Спасибо за редактирование, но мне не хватает знаний о машинах. Я планирую потратить время на то, чтобы научиться это делать в будущем. Но я подумал, что другим людям может быть интересно реализовать эту точку зрения :)

fourmisnuee 29.08.2024 11:23

Если вы собираетесь вернуть генератор, нет смысла использовать views::take_while, вы можете просто написать цикл, проверяющий предикат. И тогда особенно нет смысла использовать elements_of, просто условно co_yield еще один элемент, если он есть (вместо того, чтобы возвращать элементы поддиапазона, в котором, как вы знаете, есть не более одного элемента).

Barry 29.08.2024 15:52
Ответ принят как подходящий

Ответ на мой первоначальный вопрос

Я обнаружил, что реальная реализация того, что я назвал take_while_inclusive в своем первоначальном вопросе, была предложена для C++26 в P3220R0 под именем std::delimit (что, на мой взгляд, на самом деле гораздо лучшее имя)

Реализацию для libc++ можно найти здесь

Если у вас нет таких же ограничений (constexpr, lazy, передать только один раз), как у меня, я думаю, что другие ответы, предложенные здесь, хороши, но это решение объединяет лучшее из всех миров.


Однако

Как это часто бывает, когда мы пытаемся найти решение проблемы, в конечном итоге мы ищем решение подзадачи, не осознавая, что пытаемся решить вопрос не с той стороны. У меня так было, и я благодарю @Caleth за то, что он указал мне на это.

Если вы, как и я, на самом деле пытаетесь собрать диапазон ожидаемых значений в ожидаемый диапазон, я думаю, вам следует попробовать использовать собрать

Основное поведение:

#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);
}

Выход :

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

Другие вопросы по теме