Профиль: Аноним (вход | регистрация) неRU opennet.me  
OpenNET

[ новости /+++ | форум | теги |    ]

Первый выпуск scan, библиотеки разбора текста для C++ с построением конечного автомата при компиляции

23.09.2026 10:18 (MSK)

Опубликован первый выпуск библиотеки 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 нс

   "[email protected]"   распознавание, ничего не извлекается

     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 scan::scan<f>.scalar() написанное вручную scan::scan<f>
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


Контексты и аллокаторы

Любой объект вызывающей стороны - аллокатор, пул, арену, что угодно ещё - можно передать в тот вызов, который создаёт значение. Тип контекста при этом не теряется: состояние сканера и каждый его хук могут быть шаблонами по нему, так что в сканере не упомянут ни один тип вызывающей стороны.


   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; шаблоны работают с байтами.
  • Не поддерживаются шаблоны, известные только во время выполнения.
Сборка

Собирается в clang и GCC. Модули C++ не обязательны: доступны заголовочные файлы (include/), которые генерирует из тех же модулей утилита demodulizer - каждый push в 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)


  1. Главная ссылка к новости (https://github.com/j4niwzis/sc...)
Автор новости: j4niwzis
Лицензия: CC BY 3.0
Короткая ссылка: https://opennet.ru/66327-scan
Ключевые слова: scan, cpp
При перепечатке указание ссылки на opennet.ru обязательно


Обсуждение (5) RSS
  • 1, piteri (ok), 12:09, 23/09/2026 [ответить]  
  • +/
    А какая-то практическая польза от него есть?
     
     
  • 4, Аноним (4), 12:16, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +1 +/
    похоже на очередной аналог регулярных выражений.
     

  • 2, Аноним (2), 12:09, 23/09/2026 [ответить]  
  • +/
    Очередной стандарт C++ принят. Настало время самого интересного - придумать зачем он на практике.
     
  • 3, Аноним (3), 12:13, 23/09/2026 [ответить]  
  • –1 +/
    Подача материала, конечно, препаршивая, видимо, это показатель. А чего сравнения с flex нет?
     
  • 5, Аноним1234 (?), 12:24, 23/09/2026 [ответить]  
  • +/
    >Код проекта написан на C++23 и распространяется под лицензией GPLv3.

    Поясните, это значит что это нельзя использовать в коммерческих проектах?

     
     Добавить комментарий
    Имя:
    E-Mail:
    Текст:



    XSQUARE
    Inferno Solutions
    Hosting by Hoster.ru
    Хоcтинг:

    Закладки на сайте
    Проследить за страницей
    Created 1996-2026 by Maxim Chirkov
    Добавить новость, Поддержать