Дата публикации:Wed, 23 Sep 2026 10:18:07 +0300
Опубликован первый выпуск библиотеки scan (0.1.0), разбирающей текст в значения по шаблону, известному на этапе компиляции. Шаблон или формат записывается как аргумент шаблона C++ и превращается при компиляции в размеченный детерминированный конечный автомат (tagged deterministic finite automaton, TDFA), обход которого разворачивается по состояниям в код на этапе компиляции с использованием шаблонов C++. Результатом выполнения является значение запрошенного типа. Код проекта написан на C++23 и распространяется под лицензией GPLv3.
import scan; // Совпадает ли вся строка. Каждая точка входа - ещё и адаптор диапазона. scan::match<"[a-z]+@[a-z.]+">(address); address | scan::match<"[a-z]+@[a-z.]+">; // С группами. const auto found = scan::match<"([0-9]+)-([a-z]+)">("42-abc"); found.get<1>().to_view(); // "42" // Начало строки, занятое шаблоном, и первое совпадение где угодно. scan::starts_with<"[a-z]+">("abc123").whole().to_view(); // "abc" scan::search<"[0-9]+">("id=4210x").to_view(); // "4210" // Все совпадения и куски между ними - ленивые представления. for (const auto& one : text | scan::search_all<"[a-z]+">) { ... } const auto fields = "a,bb,,ccc" | scan::split<","> | std::ranges::to<std::vector>(); // Значения, а не текст. struct row { int id; std::string_view name; }; const row one = scan::scan<"{},{[a-z]+}">(line); // Список, сумма типов, вложенная форма. struct all { std::vector<int> values; std::variant<int, std::string_view> tail; }; const all got = scan::scan<"{{}{*,?}} {{[0-9]+}|{[a-z]+}}">("1,2,3 abc"); // Начало ввода и то, что от него осталось. const auto [value, rest] = scan::scan_prefix<"{},{}">(line).take<point>(); // По записи за раз, из чего угодно. for (const row& one : scan::each<"{},{[a-z]+}\\n">(text).of<row>()) { ... } Диапазон, который можно прочитать лишь однажды, читается без буферизации, поля собираются по мере поступления символов. Сколько символов придётся удержать, шаблон задаёт при компиляции; там, где такого числа нет, чтение отклоняется при компиляции.
std::istringstream source("set speed 42\\nset gain 7\\n"); source > std::noskipws; struct command { scan::held<16> name; int value; }; for (const command& one : scan::each<"set {[a-z]+} {[0-9]+}\\n">(std::views::istream<char>(source)) .of<command>()) { ... } Производительность. Замеры проведены на Ryzen 9 9950X, clang 22.1.8 с libc++, -O3 -march=native,LTO. Тридцать две записи за проход, медиана семи проходов.
"([a-z]+),([a-z]+),([a-z]+),([a-z]+),([a-z]+)" "alpha,bravo,charlie,delta,echo" scan::scan<f>.sentinel() 477 нс re2c 554 нс scan::scan<f> 659 нс CTRE 717 нс RE2 15986 нс те же пять полей, по двести букв каждое scan::scan<f>.sentinel() 57.8 нс re2c 637 нс CTRE 692 нс RE2 11751 нс "first.last@subdomain.example.com" распознавание, ничего не извлекается scan::match<p>.sentinel().scalar() 436 нс scan::match<p>.scalar() 545 нс re2c 1186 нс RE2 2616 нс CTRE 14339 нс Свёртка групп по ходу разбора Типу сообщается, к какой из его групп относится очередной символ, и вычисление выполняется на месте: ни одна итерация цикла не сохраняется, ни одной подстроки не создаётся.
struct tally { unsigned long value = 0; }; struct reading { tally number; std::string_view tail; }; template <> struct scan::scanner<tally> { static constexpr std::string_view pattern() { return R"(\\(((_+)(X|Y)*)*\\))"; } struct state_type { unsigned long total = 0; unsigned place = 0, marks = 0; }; static constexpr state_type begin_groups() { return {}; } static constexpr void opened_group(state_type& one, scan::group_at<0>) { one.place = 0; one.marks = 0; } static constexpr void closed_group(state_type& one, scan::group_at<0>) { unsigned long weight = 1; for (unsigned step = 1; step < one.place; ++step) weight *= 10; one.total += weight * one.marks; } static constexpr void push_group(state_type& one, scan::group_at<1>, char) { ++one.place; } static constexpr void push_group(state_type& one, scan::group_at<2>, char letter) { one.marks += letter == 'Y' ? 2u : 1u; } static constexpr tally finish_groups(state_type one) { return {one.total}; } }; const reading got = scan::scan<"value={}{[a-z]*}">("value=(__X_XX)abcdefgh").of<reading>(); // got.number.value == 12, got.tail == "abcdefgh"
heap написанное вручную 10 141 ns 41.4 ns 42.5 ns 170 ns 100 439 ns 341 ns 392 ns 1176 ns 1000 3314 ns 3170 ns 3797 ns 11887 ns
Контексты и аллокаторы Любой объект вызывающей стороны - аллокатор, пул, арену, что угодно ещё - можно передать в тот вызов, который создаёт значение. Тип контекста при этом не теряется: состояние сканера и каждый его хук могут быть шаблонами по нему, так что в сканере не упомянут ни один тип вызывающей стороны.scan::scan<f>.scalar()scan::scan<f>struct arena { std::pmr::memory_resource* where = nullptr; std::pmr::memory_resource* resource() const { return where; } }; struct numbers { std::pmr::vector<int> values; }; struct both { numbers left; numbers right; }; template <> struct scan::scanner<numbers> { static constexpr std::string_view pattern() { return "([0-9]+)(?:,([0-9]+))*"; } template <class Told> struct state { std::pmr::vector<int> values; int running = 0; }; static state<scan::default_context_t> begin_groups() { return {}; } template <class Told> requires requires(const Told& one) { one.resource(); } static state<Told> begin_groups(const Told& told) { return {std::pmr::vector<int>(told.resource()), 0}; } template <class Told, std::size_t Which> static void push_group(state<Told>& one, scan::group_at<Which>, char digit) { one.running = one.running * 10 + (digit - '0'); } template <class Told, std::size_t Which> static void closed_group(state<Told>& one, scan::group_at<Which>) { one.values.push_back(one.running); one.running = 0; } template <class Told> static numbers finish_groups(state<Told> one) { return {std::move(one.values)}; } }; std::pmr::monotonic_buffer_resource bytes; const arena mine{&bytes}; scan::scan<"{} {}">(text).of<both>(mine); // один на оба места scan::scan<"{} {}">(text).of<both>(mine, scan::default_context); // по одному на место scan::scan<"{} {}">(text).of<both>({mine, scan::default_context}); // то же в скобках const both got = scan::scan<"{} {}">(text).with(mine); // Контексту не обязательно быть своим типом: аллокатор доходит до того, // что строит чтение. struct two { std::pmr::string name; std::pmr::string tail; }; const two kept = scan::scan<"{[a-z]+} {[a-z]+}">(text).of<( std::pmr::polymorphic_allocator<>(&bytes)); Хук получает сам объект вызывающей стороны, а не копию, поэтому состояние может хранить его адрес: контекст живёт столько же, сколько вызов, а всё чтение происходит внутри этого вызова. Прочие особенности
- Два слоя: слой шаблонов (match, starts_with, search, search_all, split) и слой форматов, где "{}" - поле, а значение места определяют поля самого типа. Правило разрешения неоднозначности - leftmost-first, как в Perl, RE2 и CTRE. Всё перечисленное работает и в константных выражениях. Нет lookaround, обратных ссылок и свойств Unicode; шаблоны работают с байтами. Не поддерживаются шаблоны, известные только во время выполнения.
FetchContent_Declare(scan GIT_REPOSITORY https://github.com/j4niwzis/scan.git GIT_TAG v0.1.0) FetchContent_MakeAvailable(scan) target_link_libraries(mine PRIVATE scan::scan)
Новость позаимствована с opennet.ru
Ссылка на оригинал: https://www.opennet.ru/opennews/art.shtml?num=66327