Публикуван е първият брой на библиотеката scan (0.1.0), която преобразува текст в стойности по шаблон, известен по време на компилацията. Шаблонът или форматът се записва като аргумент на шаблон в C++ и се превръща при компилацията в маркериран детерминиран краен автомат (tagged deterministic finite automaton, TDFA), преминаването през който се реализира по състояния в код по време на компилацията с използване на шаблони C++. Резултатът от изпълнението е стойност от искания тип. Кодът на проекта е написан на C++23 и се разпространява под лицензия GPLv3. import scan; // Проверява ли се цялата стринга. Всяка входна точка - също така и адаптер на диапазон. scan::match(address); address | scan::match; // С групи. const auto found = scan::match("42-abc"); found.get().to_view(); // "42" // Начало на стринга, заето от шаблона, и първо съвпадение където и да е. scan::starts_with("abc123").whole().to_view(); // "abc" scan::search("id=4210x").to_view(); // "4210" // Всички съвпадения и парчета между тях - мързеливи представяния. for (const auto& one : text | scan::search_all) { … } const auto fields = "a,bb,,ccc" | scan::split | std::ranges::to(); // Стойности, а не текст. struct row { int id; std::string_view name; }; const row one = scan::scan(line); // Списък, сума от типове, вложена форма. struct all { std::vector values; std::variant tail; }; const all got = scan::scan("1,2,3 abc"); // Начало на входа и това, което остава от него. const auto [value, rest] = scan::scan_prefix(line).take(); // По запис за веднъж, от каквото и да било. for (const row& one : scan::each(text).of()) { … }
Диапазон, който може да се чете единствено веднъж, се чете без буферизация, полетата се събират по време на приемане на символите. Колко символа ще трябва да се задържат, шаблонът определя при компилация; там, където такова число липсва, четенето се отказва по време на компилацията. std::istringstream source("set speed 42\\nset gain 7\\n"); source > std::noskipws; struct command { scan::held name; int value; }; for (const command& one : scan::each(std::views::istream(source)) .of()) { … } Производителност.
Замерите са проведени на 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 { 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) { one.place = 0; one.marks = 0; } static constexpr void closed_group(state_type& one, scan::group_at) { 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, char) { ++one.place; } static constexpr void push_group(state_type& one, scan::group_at, 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=(__X_XX)abcdefgh»).of(); // got.number.value == 12, got.tail == «abcdefgh»
Все обекты вызывающей стороны — аллокатор, пул, арену и любые другие — могут быть переданы в вызов, который создает значение. При этом тип контекста не теряется: состояние сканера и каждый его хук могут быть шаблонами, поэтому в сканере не упоминается ни один тип вызывающей стороны. struct arena { std::pmr::memory_resource* where = nullptr; std::pmr::memory_resource* resource() const { return where; } }; struct numbers { std::pmr::vector values; }; struct both { numbers left; numbers right; }; template struct scan::scanner { static constexpr std::string_view pattern() { return "([0-9]+)(?:,([0-9]+))*"; } template struct state { std::pmr::vector values; int running = 0; }; static state begin_groups() { return {}; } template requires requires(const Told& one) { one.resource(); } static state begin_groups(const Told& told) { return {std::pmr::vector(told.resource()), 0}; } template static void push_group(state& one, scan::group_at, char digit) { one.running = one.running * 10 + (digit - '0'); } template static void closed_group(state& one, scan::group_at) { one.values.push_back(one.running); one.running = 0; } template static numbers finish_groups(state one) { return {std::move(one.values)}; } }; std::pmr::monotonic_buffer_resource bytes; const arena mine{&bytes}; scan::scan(text).of(mine); // один на оба места scan::scan(text).of(mine, scan::default_context); // по одному на место scan::scan(text).of({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(text).of<( std::pmr::polymorphic_allocator(&bytes));
Хук получает сам объект вызывающей стороны, а не его копию, так что состояние может хранить его адрес: контекст живет столько же, сколько и вызов, а все чтение происходит внутри этого вызова. Другие особенности
- Два уровня: уровень шаблонов (match, starts_with, search, search_all, split) и уровень форматов, где «{}» — это поле, а значение места определяется полями самого типа.
- Правило разрешения неоднозначностей — leftmost-first, как в Perl, RE2 и CTRE.
- Все упомянутое работает и в константных выражениях.
- Нет lookaround, обратных ссылок и свойств Unicode; шаблоны работают с байтами.
- Шаблоны, известные только во время выполнения, не поддерживаются.
Сборка
Компилира се с clang и GCC. Модулите на C++ не са задължителни: налични са заглавни файлове (include/), генерирани от същите модули от утилита demodulizer — всяко нанасяне в main презглобява, свързва и ги записва обратно. Единствената зависимост, Boost.PFR, не е необходима при активирани binding packs от C++26. 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
