Профиль: Аноним (вход | регистрация) не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 обязательно


Обсуждение (45) Ajax | 1 уровень | Линейный | +/- | Раскрыть всё | RSS
  • 1.1, piteri (ok), 12:09, 23/09/2026 [ответить] [﹢﹢﹢] [ · · · ]  
  • +5 +/
    А какая-то практическая польза от него есть?
     
     
  • 2.4, Аноним (4), 12:16, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +4 +/
    похоже на очередной аналог регулярных выражений.
     
     
  • 3.13, j4niwzis (ok), 13:06, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +3 +/
    Почему аналог? Регулярные выражения и есть, просто с поддержкой типов C++. Но и это необязательно, можно использовать тот же интерфейс, что в том же ctre.
     
  • 3.26, Норм (?), 14:53, 23/09/2026 Скрыто ботом-модератором     [к модератору]
  • –1 +/
     

  • 1.2, Аноним (2), 12:09, 23/09/2026 [ответить] [﹢﹢﹢] [ · · · ]  
  • +1 +/
    Очередной стандарт C++ принят. Настало время самого интересного - придумать зачем он на практике.
     
     
  • 2.8, Аноним (8), 12:39, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Уже принят стандарт C++26. Там это и не только можно с интроспекцией.
     
     
  • 3.22, j4niwzis (ok), 13:26, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    В C++26 есть и рефлексия, а не только интроспекция.
     

  • 1.3, Аноним (3), 12:13, 23/09/2026 [ответить] [﹢﹢﹢] [ · · · ]  
  • +/
    Подача материала, конечно, препаршивая, видимо, это показатель. А чего сравнения с flex нет?
     
     
  • 2.20, Аноним (20), 13:16, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Могу сравнить за них:
    При использовании flex нужно писать .l файл, вызывать сам flex из системы сборки, компилять и линковать.
    Если брать scan, то достаточно подключить #include <s>(а потом подождать пока компилятор разберет эту шаблонную лапшу, и молиться что по пути не возникнет ошибка)</s>
     
  • 2.21, j4niwzis (ok), 13:17, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Так re2c обычно быстрее flex. Но сравнение всё равно можно было бы добавить
     

  • 1.5, Аноним1234 (?), 12:24, 23/09/2026 [ответить] [﹢﹢﹢] [ · · · ]  
  • +2 +/
    >Код проекта написан на C++23 и распространяется под лицензией GPLv3.

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

     
     
  • 2.7, Аноним (8), 12:36, 23/09/2026 Скрыто ботом-модератором     [к модератору]
  • +/
     
  • 2.14, j4niwzis (ok), 13:08, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • –3 +/
    Вполне можно, нужно только условия лицензии соблюдать.
     
  • 2.16, Аноним (16), 13:12, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • –2 +/
    Для коммерческих целей пиши сам с нуля, а GPL это чтоб исходниками делиться.
     
     
  • 3.18, j4niwzis (ok), 13:15, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +1 +/
    Коммерция вполне возможна и когда исходниками делятся. Закрыть код и распространять бинари только вот нельзя
     
     
  • 4.45, _ (??), 18:51, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    RH - can! You are - not! ;-p
     
  • 2.23, Sm0ke85 (ok), 14:14, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +1 +/
    >Поясните, это значит что это нельзя использовать в коммерческих проектах?

    Идите лесом и пишите себе свое, а это Общественное...

     
  • 2.27, Xasd1 (?), 15:22, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    дауж…

    использовать GPT для библиотеки — это сразу выброшенное время.

    (наверно в угоду создателям SAAS только хорошо. они на сервер выложат сервис, и бинарники будто бы ни кому и не давали)

    ну LGPL же специально для библиотек есть… ну ёмаё!

    надло, расходимся

     
     
  • 3.28, j4niwzis (ok), 15:36, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    LGPL это не Library GPL а Lesser GPL. https://www.gnu.org/licenses/why-not-lgpl.html
     
     
  • 4.29, Xasd1 (?), 15:48, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    ага. потому что приложения ТОЖЕ хорошо (зачастую) выпускать под LGPL

    а не только библиотеки :-)

    однако если по глуупочти засунуть в своё приложение какую-то библиотеку на GPL…  то уже НЕ ВЫЙДЕТ всё приложение выпустить под LGPL 🙈🙈🙈

     
     
  • 5.30, j4niwzis (ok), 16:08, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Не могу придумать ни единой причины выпускать приложение под LGPL. Библиотеки же имеет смысл выпускать под LGPL лишь иногда. Тут лучше подходит GPL, т.к. достаточный уровень оригинальности, а преимущества свободному ПО давать нужно.
     
     
  • 6.42, limafresh (ok), 17:28, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Тоже не понимаю смысл использования LGPL для не-библиотек (например приложений). Смысл LGPL - позволить линковку с проприетарным кодом, а у приложений линковки как таковой нет - они не библиотеки. Выходит та же самая GPL без возможности пользоваться преимуществами послаблений LGPL. Зачем тогда так запутывать, можно было выбрать обычную GPL.
     
  • 3.41, limafresh (ok), 17:21, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    > использовать GPT для библиотеки — это сразу выброшенное время.

    Естественно. Если кто-то сделал востребованную библиотеку под GPL, то это скорее всего означает, что вскоре появится альтернатива, созданная с нуля, но под MIT, а оригинал загнётся. Т.е переписывание не ради технического превосходства, а только ради юридических нюансов, которые без воли государства - ничто. Распыление сил. Велосипединг.

     
  • 2.34, Аноним (34), 16:36, 23/09/2026 Скрыто ботом-модератором     [к модератору]
  • –1 +/
     
  • 2.36, Аноним (36), 16:40, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Именно это и значит. GPLv3 специально сделана токсичной для коммерции чтобы не дай бог успех не случился.
     
  • 2.38, limafresh (ok), 17:05, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Это распространённое заблуждение из-за манипуляции терминами, мол коммерческий = закрытый. Можно использовать в любых проектах, при соблюдении условий GPL (например открытие кода). Ведь есть коммерческие, но открытые проекты, так само, как есть и некоммерческие, но закрытые проекты.
     
     
  • 3.43, Аноним (-), 18:28, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Только вот открытое довольно сложно коммерциализировать. Это шапка себе позволить может, а мелкий разработчик — нет.
     
     
  • 4.49, limafresh (ok), 20:06, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    В чём сложно? Просто это не всем подходит. Одна из самых распространенных бизнес-моделей - бесплатная версия под (A)GPL, а платная - под коммерческой лицензией. Если кто-то не хочет раскрывать код - будет вынужден заплатить за коммерческую лицензию. Также есть такое (касается только сервисов), что приложение сервиса открыто, но услуги платные, либо частично платные. Яркий пример - Telegram и его Telegram Premium.
     
  • 4.50, limafresh (ok), 20:22, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Если этот "мелкий разработчик" разработчик сервиса, где у пользователей есть аккаунты, то монетизация может идти через платные функции или подписки (например ускоренная загрузка файлов за деньги или подписка на платные реакции на комментарии). Софт может быть хоть под WTFPL, ведь ценность не в софте, а в сервисе; софт это лишь оболочка для доступа к сервису.
     

  • 1.6, Аноним (6), 12:31, 23/09/2026 Скрыто ботом-модератором [﹢﹢﹢] [ · · · ]     [к модератору]
  • +1 +/
     

  • 1.9, Shantikov (?), 12:47, 23/09/2026 [ответить] [﹢﹢﹢] [ · · · ]  
  • +1 +/
    >Любого студента учат сначала, как минимум, описать суть проблемы, потом способы решения, затем свой способ решения и чем он лучше остальных. Тут сразу "на ешь решение"

    Видимо это была их попытка написать текст статьи без ии

     
  • 1.10, Аноним (10), 12:57, 23/09/2026 [ответить] [﹢﹢﹢] [ · · · ]  
  • +/
    Угу. Как обычно, на тестах показывает "быстрее всех", но потом в процессе применения "неожиданно" выясняется, что из-за шаблонов экзешник растёт по экспоненте, а из-за большого объема кода и производительность начинает неожиданно лагать.

    А ещё это будет безумно сложно отлаживать, если что-то пошло не так.

    А, ну и ещё компилироваться это будет вечность.

    В общем и целом, если вам нужен быстрый разбор текста - лучше и полезнее всего использовать классику, типа flex или его аналогов.

     
     
  • 2.12, j4niwzis (ok), 13:04, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Так сама детерминизация в DFA и TDFA экспоненциальная, а она в генераторах лексеров есть. Тут алгоритм, которым пользуются некоторые генераторы лексеров, а шаблонами кодген не генерирует сильно больше нужного. Проблема с ростом кода была, но была исправлена с переходом на модель с computed goto. Теперь оно нормально генерируется.

    Компилируется и правда достаточно долго, этого не избежать с шаблонами и рабором через constexpr (JIT или хотя бы более быстрый constexpr то в компиляторы не завезли).

    Подход с кодогенерацией я не уважаю, если можно сделать без нее. Тут вот можно.

     
     
  • 3.33, черпало (?), 16:26, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    > Подход с кодогенерацией я не уважаю, если можно сделать без нее.

    А зачем нагенерил нейронкой код-то, если так?

     

  • 1.11, АнОним (?), 12:59, 23/09/2026 [ответить] [﹢﹢﹢] [ · · · ]  
  • +1 +/
    >>Шаблон записывается как аргумент шаблона

    Дальше не читал

     
  • 1.15, aname (ok), 13:10, 23/09/2026 [ответить] [﹢﹢﹢] [ · · · ]  
  • +/
    Чтобы что?
     
     
  • 2.17, j4niwzis (ok), 13:14, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    Чтобы регулярками быстро текст разбирать. И с C++ типами.
     

  • 1.24, Аноним (24), 14:30, 23/09/2026 Скрыто ботом-модератором [﹢﹢﹢] [ · · · ]     [к модератору]
  • –2 +/
     
  • 1.31, Аноним (31), 16:13, 23/09/2026 [ответить] [﹢﹢﹢] [ · · · ]  
  • +/
    serde изобрёл?
     
     
  • 2.46, Аноним (46), 19:35, 23/09/2026 [^] [^^] [^^^] [ответить]  
  • +/
    скорее syn
     

  • 1.32, Аноним (32), 16:17, 23/09/2026 Скрыто ботом-модератором [﹢﹢﹢] [ · · · ]     [к модератору]
  • +/
     
  • 1.35, мяв (?), 16:39, 23/09/2026 [ответить] [﹢﹢﹢] [ · · · ]  
  • +/
    круто ! так держать
     

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



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

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