Вызов алгоритма получает временный диапазон. Определите тип результата и объясните защиту от висячего итератора:
#include <algorithm>
#include <concepts>
#include <ranges>
#include <vector>
int main() {
auto result = std::ranges::find(std::vector{1, 2, 3}, 2);
static_assert(std::same_as<decltype(result), std::ranges::dangling>);
}
result имеет тип std::ranges::dangling, а не тип итератора вектора. Временный std::vector уничтожается в конце полного выражения, поэтому возвращать итератор наружу было бы небезопасно. Ranges-алгоритм предотвращает такую ошибку на уровне типа: результат нельзя разыменовать как обычный итератор.
Классические алгоритмы STL принимают итераторы и обычно возвращают итераторы. Если передать им временный контейнер через вспомогательную функцию или временный объект, возвращённый итератор может пережить контейнер и стать висячим.
В C++20 Ranges интерфейсы алгоритмов стали учитывать свойства самого диапазона. Механизм borrowed_range позволяет отличить диапазоны, чьи итераторы могут безопасно использоваться после завершения времени жизни объекта диапазона, от остальных. Для небезопасных временных диапазонов применяется маркерный тип std::ranges::dangling.
В выражении std::vector{1, 2, 3} создаётся временный вектор. std::ranges::find находит элемент во время вызова, но после завершения полного выражения временный вектор уничтожается вместе со всеми его элементами и итераторами.
Если бы алгоритм вернул обычный итератор, следующий код выглядел бы корректно, но содержал неопределённое поведение:
Проблема особенно опасна в обобщённом коде: вызывающий код может не знать, был ли диапазон временным, и без явной проверки сохранить возвращённый итератор.
Для алгоритма std::ranges::find тип результата концептуально определяется через std::ranges::borrowed_iterator_t<R>. Если диапазон R является заимствованным, возвращается его итератор; иначе возвращается std::ranges::dangling.
Упрощённо правило выглядит так:
std::ranges::dangling — не исключение и не объект, проверяющий ошибку во время выполнения. Это специальный тип, который не предоставляет операции обычного итератора. Поэтому попытка написать *result не должна компилироваться.
Защита не означает, что все проблемы времени жизни исчезли. Можно получить висячий итератор, если вручную сохранить итератор от именованного объекта, а затем уничтожить или изменить сам контейнер. Ranges-механизм защищает конкретный сценарий передачи временного незаимствованного диапазона, но не заменяет общий контроль времени жизни.
Важно отличать borrowed_range от владения элементами. Заимствованный диапазон гарантирует безопасность итераторов относительно времени жизни объекта диапазона, но не обязательно владеет элементами. Например, представление над внешним буфером может быть не владеющим, однако его итераторы могут оставаться корректными после уничтожения самого объекта представления, если сам буфер продолжает жить.
Практический компромисс таков: интерфейс становится безопаснее и лучше выражает ограничения типов, но универсальный код должен уметь обрабатывать std::ranges::dangling. Если алгоритму нужен сам найденный элемент, а не итератор, иногда лучше использовать алгоритм или преобразование, возвращающее значение, либо сначала сохранить диапазон в именованном объекте.
В библиотечном коде функция искала первый подходящий элемент в переданном диапазоне и возвращала итератор. После перехода на ranges выяснилось, что вызов с временным контейнером больше не компилируется как обычная работа с итератором.
Рассматривались варианты:
std::ranges::dangling. Это сохраняет обобщённость и делает опасный случай видимым на этапе компиляции.Выбран третий вариант. Для API, которому нужен результат независимо от времени жизни входного диапазона, возвращали std::optional<Value>, копируя или перемещая найденное значение. Для API, работающего только с живым внешним контейнером, принимали именованный диапазон или ограничивали время жизни вызывающей стороны контрактом.
Результат: вызов с временным vector перестал создавать скрытый риск неопределённого поведения, а вызывающий код был вынужден явно выбрать безопасную семантику — сохранить диапазон или получить значение.
Вопрос: Будет ли std::ranges::find возвращать std::ranges::dangling для именованного std::vector?
Нет. Именованный std::vector используется как lvalue-диапазон, поэтому результатом будет его обычный итератор. Уничтожение временного объекта в этом случае не происходит сразу после вызова, но сам контейнер всё равно должен продолжать жить, пока используется итератор.
Вопрос: Почему тип std::ranges::dangling лучше, чем возврат нулевого итератора?
Итераторный тип не обязан иметь универсальное значение, означающее «невалиден»: у разных контейнеров разные итераторы, а сравнение с нулём для них не является общим интерфейсом. std::ranges::dangling выражает ошибочный способ использования на уровне типов и не допускает случайного разыменования. Это compile-time-защита, а не специальная проверка результата поиска.
Вопрос: Можно ли объявить собственный диапазон заимствованным?
Да, тип может специализировать std::ranges::enable_borrowed_range значением true, если разработчик действительно гарантирует, что итераторы диапазона остаются корректными после уничтожения временного объекта диапазона. Это не должно использоваться только для обхода ошибки компиляции: неверная специализация снова приведёт к возможности висячих итераторов и неопределённому поведению.