Временный std::vector передают алгоритму std::ranges::find, но получить найденный итератор нельзя. Какой механизм предотвращает висячий итератор?
Алгоритмы C++20 Ranges возвращают std::ranges::dangling, если результатом должен быть итератор в временном диапазоне, который не моделирует borrowed_range. Это предотвращает использование итератора после уничтожения временного контейнера.
Для именованного контейнера алгоритм возвращает обычный итератор, потому что время жизни контейнера контролируется вызывающим кодом. Механизм защищает от опасного результата на уровне типов, а не во время выполнения.
Классические алгоритмы STL принимают пары итераторов и обычно возвращают итератор без учёта того, откуда взят диапазон. Если диапазон создан временно, контейнер может быть уничтожен сразу после завершения вызова, а возвращённый итератор станет недействительным.
В C++20 Ranges появились алгоритмы, работающие с диапазонами как с единым объектом. Концепция borrowed_range позволяет выразить, безопасно ли сохранять итератор после завершения обработки диапазона, и перенести эту проверку в систему типов.
Вызов алгоритма над временным контейнером выглядит удобным: контейнер создаётся, обрабатывается и уничтожается в одном выражении. Однако итератор, указывающий внутрь такого контейнера, нельзя безопасно использовать после завершения полного выражения.
Без специальной защиты функция могла бы вернуть формально корректный по типу итератор, который уже ссылается на уничтоженный объект. Разыменование такого итератора приводит к неопределённому поведению; проверка времени жизни во время выполнения обычно невозможна.
Для алгоритмов ranges тип результата зависит от того, является ли переданный диапазон borrowed_range. Если диапазон безопасно сохраняет валидность своих итераторов после выхода из алгоритма, возвращается обычный итератор. Для временного std::vector это условие не выполняется, поэтому возвращается специальный пустой тип std::ranges::dangling.
Именованный std::vector передаётся как lvalue. Ссылочные диапазоны считаются заимствованными, поэтому результатом поиска становится обычный итератор, пока сам контейнер не уничтожен и операция над ним не инвалидировала этот итератор.
Минимальный пример:
std::ranges::dangling не является рабочим итератором: его нельзя разыменовать. Это намеренное ограничение, заставляющее изменить дизайн кода — сохранить контейнер в именованном объекте либо извлечь само значение другим безопасным способом.
Механизм не гарантирует вечную валидность итератора именованного контейнера. Последующая вставка, удаление, перераспределение памяти или уничтожение контейнера по-прежнему могут его инвалидировать согласно правилам конкретного контейнера.
Важно отличать временный контейнер от временного диапазона, который не владеет данными. Некоторые представления могут быть borrowed_range, если их итераторы не зависят от времени жизни самого объекта диапазона. Поэтому решение принимается по концепции типа, а не только по внешнему синтаксису вызова.
В функции поиска разработчик возвращает результат std::ranges::find по временному вектору. Вариант с попыткой вернуть найденный итератор опасен: после завершения выражения временный вектор уничтожается.
Можно вернуть копию найденного значения, но тогда нужно отдельно обработать отсутствие элемента и скопировать объект. Можно передать в функцию именованный контейнер и вернуть его итератор; это эффективно, но вызывающий код обязан гарантировать время жизни контейнера.
Предпочтительное решение зависит от интерфейса. Если нужен результат поиска, обычно возвращают значение или безопасный дескриптор, а если нужен итератор — принимают диапазон с подходящим временем жизни. std::ranges::dangling полезен тем, что не позволяет незаметно выбрать опасный вариант.
std::ranges::dangling, что поиск не выполнялся?Нет. Алгоритм может полностью выполнить поиск и найти элемент, но результат-итератор запрещён для безопасного использования после обработки временного диапазона. dangling описывает недоступность безопасного результата, а не факт или успешность поиска.
Нет. Даже если алгоритм нашёл элемент, временный контейнер уничтожается в конце полного выражения. Проверка на end не продлевает время жизни контейнера и не делает итератор валидным.
dangling?Нет. Решение зависит от того, моделирует ли тип borrowed_range, а не только от способа записи вызова. Например, lvalue контейнера считается заимствованным диапазоном, а временный std::vector — нет; отдельные не владеющие представления могут иметь собственную безопасную семантику.