Объясните механизм, из-за которого конвейер std::views::filter не выполняет фильтрацию при создании представления.
std::views::filter создаёт ленивое представление, а не новый контейнер с отфильтрованными элементами. Предикат применяется только при обращении к итераторам представления: обычно поиск первого элемента выполняется при вызове begin(), а последующие проверки — во время продвижения итератора.
Поэтому само построение конвейера почти не выполняет работу и не вызывает предикат. Фильтрация происходит в момент обхода результата.
До появления Ranges алгоритмы C++ обычно принимали пару итераторов и записывали результат в отдельный контейнер либо изменяли существующий диапазон. Это усложняло композицию операций: для каждого промежуточного результата требовались временные контейнеры, дополнительные выделения памяти и явное управление границами диапазона.
Подход C++20 Ranges разделяет описание обработки данных и её выполнение. Представления (views) позволяют строить ленивые конвейеры, в которых операции объединяются без немедленного создания промежуточных коллекций.
Ленивость легко принять за обычную фильтрацию контейнера. Если разработчик ожидает, что после создания представления элементы уже проверены, он может неправильно оценить момент побочных эффектов, стоимость операции или время возникновения исключения.
Особенно опасно помещать в предикат изменение внешнего состояния, ввод-вывод или другую операцию с побочными эффектами. Такой предикат будет вызван не при создании представления, а при конкретном обходе; повторный обход может привести к повторным вызовам.
filter_view хранит базовый диапазон и копию или перемещённый объект предиката. Оно не хранит отдельный список подходящих элементов. Итератор представления содержит итератор базового диапазона и при продвижении пропускает элементы, для которых предикат возвращает false.
Упрощённо жизненный цикл выглядит так:
begin() ищется первый подходящий элемент;Конвейеры views обычно не владеют исходным диапазоном. Поэтому исходный контейнер должен существовать дольше представления и всего времени его обхода. Изменение контейнера также может инвалидировать итераторы или изменить наблюдаемый результат согласно правилам конкретного контейнера.
Сначала выводится created, а проверки выполняются во время обхода even. Представление экономит память и позволяет объединять операции, но цена этой экономии — повторное вычисление при повторных обходах и зависимость от времени жизни исходного диапазона.
Ленивость не означает, что обход всегда дешевле материализации. Если результат используется многократно, а предикат дорогой, иногда выгоднее один раз создать отдельный контейнер с результатом. Если нужен снимок данных, независимый от исходного контейнера, материализация также является более подходящим решением.
Сервис получает большой набор записей и должен вывести только активные записи, затем выбрать из них записи нужного типа. Вариант с двумя промежуточными контейнерами прост для отладки, но расходует дополнительную память и выполняет несколько проходов по данным.
Вариант с конвейером filter и transform не создаёт промежуточные контейнеры и обрабатывает элементы по мере вывода. Это удобно для однократного последовательного чтения, но предикаты и преобразования будут выполняться заново при каждом новом обходе.
Если вывод может быть повторён много раз, а проверки включают дорогой доступ к внешнему ресурсу, выбранным решением становится однократная материализация результата. Она занимает больше памяти, зато фиксирует данные и исключает повторные вычисления. Для однократной обработки в памяти предпочтительнее ленивый конвейер, при условии что исходный диапазон живёт достаточно долго и предикаты не имеют нежелательных побочных эффектов.
Нет. При обычном обходе предикат обычно проверяет каждый рассматриваемый элемент, но один и тот же элемент может быть проверен снова при новом обходе представления. Стандарт не обещает кэширование результата фильтрации, поэтому рассчитывать на однократный вызов нельзя.
Кроме того, реализация может вызывать предикат в моменты, связанные с операциями над итераторами представления. Код должен быть корректным при повторных вызовах и не зависеть от скрытого количества проверок.
Нет, если после выхода из функции базовый контейнер уничтожается. Представление обычно содержит ссылку или итераторы к исходному диапазону, поэтому его последующий обход обращается к уничтоженному объекту и приводит к неопределённому поведению.
Безопасные варианты — вернуть независимый контейнер с материализованным результатом либо обеспечить, чтобы исходный диапазон владел данными и жил дольше представления. Сам факт, что тип представления можно вернуть по значению, не гарантирует безопасность времени жизни его источника.
filter_view исходный контейнер?Нет, сама фильтрация не удаляет и не переставляет элементы. Представление лишь определяет, какие позиции исходного диапазона видны через его итераторы.
Однако предикат может содержать изменяющий внешний объект код, а неконстантный обход может позволять изменять сами элементы через возвращаемую ссылку. Такие изменения могут повлиять на последующий обход и даже нарушить требования к диапазону, если они инвалидируют его итераторы. Поэтому фильтрацию, изменение данных и управление временем жизни следует рассматривать как отдельные аспекты.