В прошлом посте
stride_view отдавал из end() пустую stride_sentinel{}, а итератор сам таскал внутри end_ и сравнивался с ним. Это уже был sentinel — просто самый скромный его вид. Сегодня посмотрим, на что sentinel способен, когда условие конца живёт в нём самом.🍬 Итератор говорит «где», sentinel — «когда стоп»
Обычный
end() в STL — это итератор на позицию за последним элементом. Чтобы его получить, позицию надо знать заранее.Sentinel ломает это правило. Он не хранит позицию — он хранит условие остановки. Итератор шагает вперёд и на каждом шаге как бы спрашивает: «мы уже приехали?». Sentinel отвечает, глядя не на свой адрес, а на то, куда сейчас показывает итератор.
🍴 Классика жанра — C-строки
У C-строки нет заранее известного конца: он определяется на лету — идём, пока не встретим
'\0'. Идеальный кейс для sentinel:struct null_sentinel_t {
// 🔴 Сравниваем не адреса, а СОДЕРЖИМОЕ:
// итератор == концу, если он смотрит на '\0'
bool operator==(const char* ptr) const { return *ptr == '\0'; }
};
inline constexpr null_sentinel_t null_sentinel{};
struct cstring_range {
const char* ptr;
const char* begin() const { return ptr; } // обычный итератор
null_sentinel_t end() const { return null_sentinel; } // sentinel другого типа!
};
// Использование
const char* str = "hello";
for (char c : cstring_range{str}) {
std::cout << c; // hello
}❗️Обрати внимание:
begin() возвращает const char*, а end() — null_sentinel_t. Разные типы! Range-based for это разрешает ещё с C++17, а C++20 добавляет it != null_sentinel сам разворачивается в !null_sentinel.operator==(it). Пишешь один operator== — и обе стороны, и != работают бесплатно.❓ Зачем вообще так заморачиваться
🥝 Ленивый конец. Не нужно звать
strlen и проходить строку дважды — конец «находится» прямо во время итерации.♾️ Бесконечные диапазоны. У генераторов, потоков,
views::iota без верхней границы конца-позиции просто нет. Sentinel — единственный способ вообще описать такой диапазон.⚡️ Дешёвое сравнение. Проверить условие (
== '\0', счётчик дошёл до нуля) часто быстрее, чем сравнивать два итератора.🥳 В стандартной библиотеке это уже есть
Свой sentinel писать нужно не всегда. В STL живёт
std::default_sentinel_t (объект std::default_sentinel) — та самая пустая заглушка, которой пользуются counted_iterator, istream_view и куча вьюх. А для честно бесконечных диапазонов есть std::unreachable_sentinel — «мы не доедем никогда», и компилятор с чистой совестью выкидывает проверки границ.🫰
std::ranges поддерживает нестандартные sentinel-ы из коробки: достаточно, чтобы для пары <итератор, sentinel> выполнялся концепт std::sentinel_for — и твой тип поедет во все алгоритмы ranges наравне с обычными итераторами.📍Навигация: Вакансии • Задачи • Собесы
Библиотека C/C++ разработчика
#под_капотом