О понятии Sentinel говорят мало, особенно в русскоязычном пространстве. Вместе с Юрием Вашинко, опытным тимлидом и спикером нашего курса «С++ разработчик» сегодня рассмотрим, что такое Sentinel и как его использовать:
В каждом разработчике С++ (до 17 версии) прочно укоренилось понятие итератора, на котором построена по сути вся работа со контейнерами в STL. Но бывают случаи, когда приходится отказываться от использования алгоритмов в угоду собственных реализаций, т.к. они оказываются более выгодными с точки зрения эффективности.
Для начала давайте напомним всем известные понятия. Контейнеры предоставляют нам итераторы begin()
и end()
, где begin()
указывает на первый элемент контейнера, а end()
на «мифический» элемент, который находится сразу за последним элементом. И вот когда начинающие разработчики с этим знакомятся — тут бывает много интересного. Все представляют в первую очередь std::vector
, и там можно представить, что находится в конце. Но если спросить — а что же будет, если этим контейнером является std::map
— ответы будут разные. Да и объяснить бывает непросто, что этот элемент, по сути, нереальный.
Лично для меня итератор end()
вообще выбивается из хардкорного, строго типизированного и структурированного языка С++. На момент создания и реализации данной концепции это было хорошим решением, но времена меняются и задачи тоже. Подумаем, а что нам делать, если обрабатывать массив нужно не весь, а закончить обработку в зависимости от значения внутри вектора. Или, к примеру, мы хотим создать бесконечный массив, то есть постоянно дополняемый. Что в таком случае считать признаком окончания? И вот для такого рода задач понимаешь, что нет ничего лучше чем прохода в виде while(true)
и выходом по условию.
Для примера возьмем задачу, где нам нужно первое слово в предложении преобразовать в uppercase. (Опустим здесь зачем это может понадобится. Многообразие задач заказчика настолько велико, что самая нереальная и порой глупая задача на сегодняшний день может стать острой необходимостью завтра 🙂)
Решим ее при помощи цикла while
:
char introString[] = "Hello sentinel";
int i = 0;
while (introString[i]!=' ' && introString[i] != '\0')
{
introString[i] = toupper(introString[i]);
i++;
}
Если решать такую задачу через алгоритмы stl — придётся сначала найти, где находится ‘ ‘, а потом вызвать соответствующий алгоритм для прохода по данным в этой области ограничив его итераторами, что потенциально может привести полному двойному проходу по данным.
std::vector<char> vectorArray = {'H','e','l','l','o',' ','s','e','n','t','i','n','e','l'};
auto findex = std::find(vectorArray.begin(), vectorArray.end(),' ');
std::transform(vectorArray.begin(), findex, vectorArray.begin(), to_uppercase);
Символ ‘ ‘ здесь играет роль границы, когда нужно прекращать обработку массива.
И вот мы пришли по сути к понятию sentinel (в дословном переводе - «страж»). Sentinel также называют флагом или сигналом завершения обработки. И теперь давайте представим, что нам нужно в алгоритмы stl передавать не end()
, именно некий sentinel, который можно просто сравнить с итератором. Что нам это даст? В первую очередь, мы можем не задумываться о конечном итераторе в принципе и тип Sentinel может вообще не является итератором. Давайте перепишем наш пример с использованием отдельного класса Sentinel:
class Sentinel {
public:
bool operator==(std::vector<char>::iterator it) const {
return *it == ' ';
}
};
std::vector<char> vectorArray = {'H','e','l','l','o',' ','s','e','n','t','i','n','e','l'};
Sentinel sentinel;
for (auto it = vectorArray.begin();it!=sentinel;++it)
{
*it = std::toupper(*it);
}
Таким образом, мы подошли к понятию Sentinel в том виде, в котором оно используется в C++20.
Так оно объявлено в стандарте:
template< class S, class I >
concept sentinel_for = std::semiregular<S> && std::input_or_output_iterator<I> &&
__WeaklyEqualityComparableWith<S, I>;
Это концепт, который накладывает ограничение, которое означает, что в роли Sentinel может выступать объект, который можно сравнить с итератором.
Давайте модифицируем немного класс Sentinel, чтобы он смог останавливать обработку не только по символу ‘ ‘, но и по концу итератора
template <typename T>
struct Sentinel {
bool operator==(T Iter) const {
return Iter == ContainerEnd || *Iter == ' ';
}
T ContainerEnd;
};
А теперь, используя класс Sentinel, решим изначальную задачу.
std::vector<char> vectorArray = { 'H','e','l','l','o',' ','s','e','n','t','i','n','e','l' };
auto res = std::ranges::for_each(
vectorArray.begin(), Sentinel{ vectorArray.end() }, [](auto &val) {
val = toupper(val);
}
);
Более того, мы можем это использовать и в других алгоритмах:
std::ranges::sort(vectorArray.begin(), Sentinel{ vectorArray.end() }, std::greater());
Но и это еще не все — наш «мифический» end()
тоже может выступать в качестве sentinel (хотя у нас уже это заложено в классе Sentinel), и так по умолчанию и происходит, когда мы вызываем, к примеру, метод ranges::sort()
std::ranges::sort(vectorArray, std::greater());
Но в данном случае end()
приобретает совсем другой смысл, он хоть и остается неким абстрактным понятием, но может быть свободно заменен на нечто более ощутимое и связанное именно с реальными данными.
И сейчас, после того, как мы разобрали это понятие, становится очевидным, что использовать бесконечные массивы не так уж и сложно даже с алгоритмами.
Введение концепции Sentinel в C++20 значительно расширяет возможности работы с контейнерами и итераторами. Sentinel позволяет более гибко управлять границами последовательностей, заменяя «мифический» итератор end() на более выразительный объект, который не обязательно должен быть итератором. Это решение делает алгоритмы STL более эффективными, особенно при работе с частичными или бесконечными структурами данных. В итоге, Sentinel не только упрощает код, но и открывает новые возможности для создания производительных и понятных решений в современных приложениях на C++.
А подробнее о нюансах использования Sentinel и других особенностях языка С++ — на нашем курсе «Разработчик С++», старт — 28 октября. Подробности — по ссылке.