È stato pubblicato il primo rilascio della libreria scan (0.1.0), che analizza il testo in valori secondo un modello noto in fase di compilazione. Il modello o formato è scritto come argomento di un modello C++ e si trasforma in un automa finito deterministico contrassegnato (tagged deterministic finite automaton, TDFA) durante la compilazione, il cui attraversamento viene espanso in stati nel codice in fase di compilazione utilizzando modelli C++. Il risultato dell'esecuzione è il valore del tipo richiesto. Il codice del progetto è scritto in C++23 e distribuito sotto licenza GPLv3. import scan; // Verifica se l'intera stringa corrisponde. Ogni punto di ingresso è anche un adattatore di intervallo. scan::match(address); address | scan::match; // Con i gruppi. const auto found = scan::match("42-abc"); found.get().to_view(); // "42" // Inizio della stringa occupato dal modello e prima corrispondenza ovunque. scan::starts_with("abc123").whole().to_view(); // "abc" scan::search("id=4210x").to_view(); // "4210" // Tutte le corrispondenze e pezzi tra di esse - rappresentazioni pigre. for (const auto& one : text | scan::search_all) { … } const auto fields = "a,bb,,ccc" | scan::split | std::ranges::to(); // Valori, non testo. struct row { int id; std::string_view name; }; const row one = scan::scan(line); // Lista, somma di tipi, forma annidata. struct all { std::vector values; std::variant tail; }; const all got = scan::scan("1,2,3 abc"); // Inizio dell'input e ciò che rimane da esso. const auto [value, rest] = scan::scan_prefix(line).take(); // Per registrazione alla volta, da qualsiasi cosa. for (const row& one : scan::each(text).of()) { … }
Un intervallo che può essere letto solo una volta, viene letto senza buffering, i campi vengono raccolti man mano che arrivano i caratteri. Quanti caratteri bisogna mantenere, il modello lo specifica in fase di compilazione; dove tale numero non esiste, la lettura viene rifiutata in fase di compilazione. 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()) { … } Prestazioni.
Le misurazioni sono state effettuate su Ryzen 9 9950X, clang 22.1.8 con libc++, -O3 -march=native, LTO. Trenta registrazioni per esecuzione, mediana di sette esecuzioni. «([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 gli stessi cinque campi, duecento lettere ciascuno scan::scan<f>.sentinel() 57.8 ns re2c 637 ns CTRE 692 ns RE2 11751 ns «first.last@subdomain.example.com» riconoscimento, niente viene estratto scan::match<p>.sentinel().scalar() 436 ns scan::match<p>.scalar() 545 ns re2c 1186 ns RE2 2616 ns CTRE 14339 ns Raggruppamento di gruppi durante l'analisi
Il tipo informa a quale delle sue gruppi appartiene il carattere corrente e l'elaborazione avviene in loco: nessuna iterazione del ciclo viene salvata, nessuna sottostringa viene creata. 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"
Qualsiasi oggetto chiamante - allocatore, pool, arena, qualsiasi altra cosa - può essere passato a quella chiamata che crea un valore. Il tipo di contesto, in questo caso, rimane intatto: lo stato dello scanner e ciascun suo hook possono essere modelli al suo riguardo, quindi nessun tipo di chiamante è menzionato nello scanner. 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 per entrambi i posti scan::scan(text).of(mine, scan::default_context); // uno per ogni posto scan::scan(text).of({mine, scan::default_context}); // stesso in parentesi const both got = scan::scan(text).with(mine); // Il contesto non deve necessariamente essere del proprio tipo: l'allocatore arriva a ... // che costruisce la lettura. struct two { std::pmr::string name; std::pmr::string tail; }; const two kept = scan::scan(text).of<( std::pmr::polymorphic_allocator(&bytes));
L'hook riceve l'oggetto chiamante stesso e non una copia, quindi lo stato può memorizzare il suo indirizzo: il contesto vive per la stessa durata della chiamata, e tutta la lettura avviene all'interno di questa chiamata. Altre caratteristiche
- Due strati: uno strato di template (match, starts_with, search, search_all, split) e uno strato di formati, dove "{}" è un campo e il valore del posto è determinato dai campi dello stesso tipo.
- La regola di risoluzione dell'ambiguità è leftmost-first, come in Perl, RE2 e CTRE.
- Tutto ciò che è stato menzionato funziona anche nelle espressioni costanti.
- Niente lookaround, riferimenti inversi e proprietà Unicode; i template funzionano con i byte.
- Non sono supportati template noti solo durante l'esecuzione.
Compilazione
Compila in clang e GCC. I moduli C++ non sono obbligatori: sono disponibili i file di intestazione (include/) generati dalla stessa utility demodulizer — ogni push in main ricompila, collega e li committa di nuovo. L'unica dipendenza, Boost.PFR, non è necessaria con i binding pack attivati da 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)
Fonte: opennet.ru
