Привет, Хабр!
Сегодня мы рассмотрим без преувеличений один из самых недооценённых алгоритмов стандартной библиотеки — std::search
. Разберёмся, как он устроен, где реально экономит процессорные тики, а где лучше заменить его на что‑то иное.
Что делает std::search
Алгоритм ищет первое вхождение подпоследовательности [s_first, s_last)
в диапазоне [first, last)
. Если «иголка» пустая, вернётся first
, если вхождения нет — last
. В базовой форме сравнение производится через operator==
; при необходимости можно подставить предикат или целый поисковик. С C++20 почти все перегрузки search
стали constexpr
, так что теперь алгоритм доступен даже в static_assert
‑ах.
Сложность в лобовом случае не радует: максимум N × S
сравнений, где N = distance(first, last)
, S = distance(s_first, s_last)
— типичная квадратика. Поэтому важно понимать, в каких ситуациях это приемлемо, а где надо выкатывать более хитрые searcher‑ы или вообще другой алгоритм.
Обзор всех перегрузок
Перегрузка |
Что делает |
Примечание |
---|---|---|
|
Наивный поиск через |
constexpr с C++20 |
|
То же, но с пользователским предикатом |
Используйте для игнорирования регистра |
|
Параллельное/векторное исполнение |
Следует быть осторожным с потокобезопасностью |
|
Делегирует работу объекту‑поисковику |
Поддерживает Boyer‑Moore и BMH |
Последняя перегрузка — отлична при больших паттернах. В стандарт поставили три searcher‑а: default_searcher
(наивный), boyer_moore_searcher
и boyer_moore_horspool_searcher
. Они живут в <functional>
и требуют C++17. На длинных строках ускорение в несколько раз — норма.
Базовый пример: простой поиск подстроки
#include <algorithm>
#include <string_view>
#include <iostream>
bool contains(std::string_view haystack, std::string_view needle)
{
return std::search(haystack.begin(), haystack.end(),
needle.begin(), needle.end()) != haystack.end();
}
int main()
{
std::string_view text = "Для тех, кто уже знает STL";
std::cout << std::boolalpha
<< contains(text, "STL") // true
<< '\n'
<< contains(text, "Rust"); // false
}
Кода пять строк, побочных эффектов ноль. Если «иголка» пуста, вернём begin, что иногда удобно для валидации входных данных.
Пользовательский предикат: игнорируем регистр
Часто нужно искать подстроку независимо от регистра, например "http"
в строке. Это можно решить, передав в std::search
пользовательский предикат:
#include <algorithm>
#include <cctype>
#include <string>
auto ci_equal = [](char a, char b)
{
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
};
bool ci_contains(const std::string& haystack, const std::string& needle)
{
return std::search(haystack.begin(), haystack.end(),
needle.begin(), needle.end(),
ci_equal) != haystack.end();
}
std::search
ищет подпоследовательность с учётом предиката, в нашем случае — сравнение символов без учёта регистра.
std::tolower
требует unsigned char
, иначе возможен UB при работе с байтами >127 в некоторых локалях.
Производительность: когда брать boyer_moore_searcher
На больших входах (N > 1 000
и S > 3
) наивный алгоритм начинает тухнуть. Boyer‑Moore пропускает существенные куски входа благодаря таблицам смещений. В моих замерах на журналируемом логе 5 МБ ускорение составило ~6× по сравнению с базовым search
. Профиль был чистый: всё упиралось в memcmp
вместо каскада сравнений символов.
Пример:
#include <algorithm>
#include <functional>
#include <string_view>
std::size_t bm_find(std::string_view haystack, std::string_view needle)
{
using searcher = std::boyer_moore_searcher<
std::string_view::iterator, std::string_view::iterator>;
auto it = std::search(haystack.begin(), haystack.end(),
searcher(needle.begin(), needle.end()));
return it == haystack.end() ? std::string_view::npos
: std::distance(haystack.begin(), it);
}
Создавать searcher лучше один раз и переиспользовать, если шаблон повторяется.
Параллельный поиск
С C++17 к большинству алгоритмов, включая search
, добавили перегрузки с ExecutionPolicy
. На десктопе параллельная версия даёт профит, если:
Размер входа измеряется мегабайтами.
У вас не монолитная строка, а, скажем,
std::vector<int>
с тяжёлым предикатом.Вы готовы к оверхеду синхронизации.
#include <algorithm>
#include <execution>
#include <vector>
auto it = std::search(std::execution::par_unseq,
data.begin(), data.end(),
pattern.begin(), pattern.end());
Если предикат не свободен от побочных эффектов, параллельный запуск — запретная зона. В противном случае получите гонки и весёлый undefined behaviour.
ranges::search
С C++20 к нам пришли ranges. Функция‑объект std::ranges::search
возвращает std::ranges::subrange
, что избавляет от гимнастики с distance
:
#include <ranges>
#include <string_view>
auto sub = std::ranges::search(haystack, needle);
if (!sub.empty()) {
// sub.begin() даёт позицию
}
ranges‑вариант пока не поддерживает поисковые объекты типа boyer_moore_searcher
. Если нужен максимум скорости,то просто остаёмся на классическом API.
Кейс 1: валидация бинарного протокола
Когда пишем протокол поверх TCP, полезно быстро находить стартовый маяк пакета вроде 0xDE,0xAD,0xBE,0xEF
. Код предельно прямолинеен:
bool has_magic(const std::vector<std::uint8_t>& buf)
{
constexpr std::uint8_t magic[] {0xDE, 0xAD, 0xBE, 0xEF};
auto it = std::search(buf.begin(), buf.end(),
std::begin(magic), std::end(magic));
return it != buf.end();
}
Гарантий по выравниванию не нужно, потому что работаем через итераторы, а сравнение идёт байт к байту.
Кейс 2: сканирование лога mmap-ом
Если лог большой, память экономим за счёт mmap
. Ищем строку «ERROR»:
#include <fcntl.h>
#include <sys/mman.h>
#include <algorithm>
#include <string_view>
void scan_log(const char* path)
{
int fd = open(path, O_RDONLY);
off_t size = lseek(fd, 0, SEEK_END);
char* map = static_cast<char*>(mmap(nullptr, size, PROT_READ,
MAP_PRIVATE, fd, 0));
std::string_view log{map, static_cast<std::size_t>(size)};
auto it = std::search(log.begin(), log.end(),
"ERROR", "ERROR" + 5);
if (it != log.end()) {
// нашли проблему, репортим
}
munmap(map, size);
close(fd);
}
Файловый ввод‑вывод мы свели к нескольким системным вызовам, всё остальное — чистый std::search
.
Кейс 3: фильтрация HTTP-заголовков
Зачастую нужно выбросить запросы с запрещёнными заголовками. С код типа strcasestr
давно не катит; пишем на C++:
#include <string_view>
#include <algorithm>
bool contains_header(std::string_view request,
std::string_view header)
{
auto pred = [](char a, char b)
{
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
};
return std::search(request.begin(), request.end(),
header.begin(), header.end(),
pred) != request.end();
}
Функция не аллоцирует память, не ломает константность буфера и прозрачна для санитайзеров.
Некоторые проблемы
Первое, c чем регулярно сталкиваются: «иголка» размером в один элемент и пустой диапазон. Для одиночного байта выгоднее сразу брать std::find
, а не тянуть на сцену весь search
. Плюс не забываем, что пустая подпоследовательность по стандарту возвращает first
; если ваши инварианты этого не ждут, отладка превратится в квест.
Вторая группа проблем касается контекста исполнения. std::search
работает с байтами, поэтому для UTF-8 нужен слой декодирования или специализированная библиотека. На коротких паттернах Boyer‑Moore теряет преимущество из‑за стоимости предобработки — порог имеет смысл измерять профилем. И параллельный запуск с предикатом, зависящим от I/O, обнуляет потенциальный выигрыш и рискует привести к гонкам; в таких сценариях остаёмся на последовательной версии.
Итоги
std::search
— рабочий инструмент для:
линейного сканирования буферов;
навигации по mmap‑файлам без копий;
быстрой фильтрации потока данных;
построения кастомных поисков с нулевой аллокацией.
Выбирайте перегрузку под задачу: наивный поиск для мелочи, Boyer‑Moore для тяжёлых строк, par_unseq
для гигабайтных массивов, ranges::search
для адекватного и современного кода.
Если вы уже знакомы с C++ и хотите проверить свои знания, приглашаем вас пройти тестирование. Это возможность оценить свои текущие навыки и углубить понимание ключевых аспектов языка.
Также предлагаем вам присоединиться к открытым урокам:
Оптимизация производительности на C++ — 6 августа в 20:00. Разберемся, как улучшить скорость работы ваших приложений на C++.
Контейнеры C++ — 20 августа в 20:00. Узнаем, какие контейнеры наиболее эффективны для разных задач и как их правильно использовать.
Чтобы узнать больше о курсах по C++ и получить доступ к записям открытых уроков, переходите в телеграм‑бот.
Комментарии (3)
sergio_nsk
01.08.2025 13:54auto
ci_equal = [](
char
a,
char
b)
Это должно быть
auto
ci_equal = [](
unsinged char
a,
unsigned char
b)
и никаких
static_cast
<
unsigned char
>(a)
внутри не нужно."На локалях с отрицательными байтами" - какой-то бред.
Playa
01.08.2025 13:54Вообще-то всё правильно было, вы просто заменили явное приведение signed -> unsigned неявным.
unreal_undead2
В принципе UTF-8 гарантирует, что если needle и hasytack являются валидными UTF-8 строками, простой байтовый поиск найдёт корректную подстроку (поскольку старшие биты однозначно определяют, является ли данный байт отдельным символом, первым байтом многобайтовой последовательности или одним из следующих байтов). Позицию при этом получим в байтах, а не в символах - от задачи зависит, устроит ли такое.