Я работал над простым парсером, используя 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 превосходен в общих случаях, я также забыл упомянуть, что хотел бы иметь возможность использовать его во время компиляции.
@Калет Я не возвращаюсь range<variant<int, string>>
, на самом деле я возвращаю диапазон expected<int, error>
. Это довольно распространенная вещь в языке, таком как ржавчина, где диапазоны являются способом выполнения действий по умолчанию. Но, если оставить в стороне мой вариант использования, по моему мнению, это представление может быть полезно во многих сценариях.
Я не уверен, что это вообще полезно. Вы отбрасываете информацию о том, что большинство элементов передают предикат, а последний — нет. Звонящим придется заново проделать большую часть работы, которую вы уже проделали.
Вызывающий просто должен проверить, является ли последний элемент ошибкой, если я использую take_ while_inclusive, а в противном случае просто преобразовать его в диапазон int. на самом деле одна из наиболее часто используемых функций в Rust — это сбор, которая делает именно именно это. Я думаю, что другой способ понять меня — задать себе вопрос: как бы вы выполнили этот анализ, используя диапазоны?
Я бы не стал. Это завершающее действие, приводящее к диапазону и возможной ошибке. Это не то же самое, что collect
, которое приводит к Expected<Collection<T>, E>
, т. е. отбрасывает допустимые элементы, если один из них является ошибкой.
Но я хочу отказаться от него, если это ошибка! ^^ (или, скажем иначе, собирать в пределах ожидаемого диапазона). Но да, часть сбора можно выполнить в функции try_parse, и она вернет только Expected<Collection<T>, E>, это справедливо :)
Давайте продолжим обсуждение в чате.
Для 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())
);
Спасибо за ваш ответ! Я уже думал об этом решении, но пытался следовать принципам представлений, создавая значения на лету и пересекая диапазон только один раз. Я забыл упомянуть эти ограничения в своем вопросе, поэтому собираюсь его отредактировать.
На этом этапе можно было бы использовать ranges::find_if_not
вместо представления.
Вы можете гарантировать, что он будет лениво оцениваться, написав его как генератор.
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, поэтому мне хотелось бы увидеть реализацию собственного представления, но в общем случае я думаю, что это очень хороший ответ. Я отредактирую свой вопрос =)
Спасибо за редактирование, но мне не хватает знаний о машинах. Я планирую потратить время на то, чтобы научиться это делать в будущем. Но я подумал, что другим людям может быть интересно реализовать эту точку зрения :)
Если вы собираетесь вернуть генератор, нет смысла использовать views::take_while
, вы можете просто написать цикл, проверяющий предикат. И тогда особенно нет смысла использовать elements_of
, просто условно co_yield
еще один элемент, если он есть (вместо того, чтобы возвращать элементы поддиапазона, в котором, как вы знаете, есть не более одного элемента).
Я обнаружил, что реальная реализация того, что я назвал 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]
Использование вашего примера кажется подозрительным. Более разумно было бы вернуть
struct parsed_ints { range<int> numbers; optional<string> first_error; }
, а неrange<variant<int, string>>
или что-то в этом роде.