Se ha publicado la primera versión de la biblioteca scan (0.1.0), que analiza el texto en valores según una plantilla conocida en el momento de la compilación. La plantilla o formato se escribe como un argumento de plantilla en C++ y se convierte en un autómata finito determinista etiquetado (tagged deterministic finite automaton, TDFA) durante la compilación. Su recorrido se despliega a través de los estados en el código en la etapa de compilación utilizando plantillas de C++. El resultado de la ejecución es un valor del tipo solicitado. El código del proyecto está escrito en C++23 y se distribuye bajo la licencia GPLv3. import scan; // Verifica si toda la cadena coincide. Cada punto de entrada es además un adaptador de rango. scan::match(address); address | scan::match; // Con grupos. const auto found = scan::match("42-abc"); found.get().to_view(); // "42" // Inicio de la cadena, ocupada por la plantilla, y la primera coincidencia en cualquier lugar. scan::starts_with("abc123").whole().to_view(); // "abc" scan::search("id=4210x").to_view(); // "4210" // Todas las coincidencias y fragmentos entre ellas son representaciones perezosas. for (const auto& one : text | scan::search_all) { … } const auto fields = "a,bb,,ccc" | scan::split | std::ranges::to(); // Valores, no texto. struct row { int id; std::string_view name; }; const row one = scan::scan(line); // Lista, suma de tipos, forma anidada. struct all { std::vector values; std::variant tail; }; const all got = scan::scan("1,2,3 abc"); // Inicio de la entrada y lo que queda de ella. const auto [value, rest] = scan::scan_prefix(line).take(); // Por registro a la vez, de cualquier cosa. for (const row& one : scan::each(text).of()) { … }
Un rango que solo se puede leer una vez se lee sin almacenamiento en búfer, los campos se recopilan a medida que llegan los caracteres. La cantidad de caracteres a mantener es determinada por la plantilla en la compilación; donde no existe tal cantidad, la lectura es rechazada en la compilación. 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()) { … } Rendimiento.
Las mediciones se realizaron en Ryzen 9 9950X, clang 22.1.8 con libc++, -O3 -march=native, LTO. Treinta y dos registros por pase, mediana de siete pases. «([a-z]+),([a-z]+),([a-z]+),([a-z]+),([a-z]+)» «alpha,bravo,charlie,delta,echo» scan::scan<f>.sentinel() 477 ns re2c 554 ns scan::scan<f> 659 ns CTRE 717 ns RE2 15986 ns los mismos cinco campos, doscientos caracteres cada uno scan::scan<f>.sentinel() 57.8 ns re2c 637 ns CTRE 692 ns RE2 11751 ns «first.last@subdomain.example.com» reconocimiento, no se extrae nada scan::match<p>.sentinel().scalar() 436 ns scan::match<p>.scalar() 545 ns re2c 1186 ns RE2 2616 ns CTRE 14339 ns Agrupación de grupos durante el análisis.
A cada tipo se le informa a qué grupo pertenece el siguiente símbolo, y el cálculo se realiza en su lugar: no se guarda ninguna iteración del ciclo, no se crea ninguna subcadena. 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»
Cualquier objeto del llamador — un asignador, un pool, una arena, lo que sea — puede ser pasado a la llamada que crea un valor. El tipo de contexto no se pierde en este proceso: el estado del escáner y cada uno de sus hooks pueden ser plantillas a partir de él, de modo que ningún tipo del llamador se menciona en el escáner. 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); // uno para ambos lugares scan::scan(text).of(mine, scan::default_context); // uno por lugar scan::scan(text).of({mine, scan::default_context}); // lo mismo entre paréntesis const both got = scan::scan(text).with(mine); // El contexto no tiene que ser su propio tipo: el asignador llega a construir la lectura. struct two { std::pmr::string name; std::pmr::string tail; }; const two kept = scan::scan(text).of<(std::pmr::polymorphic_allocator(&bytes));
El hook recibe el objeto del llamador en sí, no una copia, por lo que el estado puede almacenar su dirección: el contexto vive tanto como la llamada, y toda la lectura ocurre dentro de esta llamada. Otras características
- Dos capas: la capa de plantillas (match, starts_with, search, search_all, split) y la capa de formatos, donde «{}» es el campo y el valor del lugar lo determinan los campos del propio tipo.
- La regla de resolución de ambigüedades es leftmost-first, como en Perl, RE2 y CTRE.
- Todo lo mencionado también funciona en expresiones constantes.
- No hay lookaround, referencias inversas ni propiedades de Unicode; las plantillas funcionan con bytes.
- No se admiten plantillas que sean conocidas únicamente en tiempo de ejecución.
Compilación
Se compila en clang y GCC. Los módulos de C++ no son obligatorios: están disponibles los archivos de encabezado (include/) que genera la utilidad demodulizer a partir de los mismos módulos. Cada push a main recompila, vincula y los commitea de nuevo. La única dependencia, Boost.PFR, no es necesaria con los paquetes de binding activados de 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)
Fuente: opennet.ru
