Primul volum al bibliotecii scan (0.1.0) a fost publicat, analizând textul în valori conform unui șablon cunoscut în timpul compilării. Șablonul sau formatul este scris ca un argument de șablon C++ și se transformă în timpul compilării într-un automat finit determinist marcat (tagged deterministic finite automaton, TDFA), al cărui parcurs se desfășoară pe stări în cod în timpul compilării utilizând șabloane C++. Rezultatul execuției este o valoare de tip solicitat. Codul proiectului este scris în C++23 și este distribuit sub licența GPLv3. import scan; \/\/ Se potrivește întreaga linie. Fiecare punct de intrare este de asemenea un adaptor de interval. scan::match<"[a-z]+@[a-z.]+">(address); address | scan::match<"[a-z]+@[a-z.]+">; \/\/ Cu grupuri. const auto found = scan::match<"([0-9]+)-([a-z]+)">("42-abc"); found.get<1>().to_view(); \/\/ "42" \/\/ Începutul liniei, ocupat de șablon, și prima potrivire oriunde. scan::starts_with<"[a-z]+">("abc123").whole().to_view(); \/\/ "abc" scan::search<"[0-9]+">("id=4210x").to_view(); \/\/ "4210" \/\/ Toate potrivirile și bucățile dintre ele — reprezentări leneșe. for (const auto& one : text | scan::search_all<"[a-z]+">) { … } const auto fields = "a,bb,,ccc" | scan::split<","> | std::ranges::to<std::vector>(); \/\/ Valori, nu text. struct row { int id; std::string_view name; }; const row one = scan::scan<"{},{[a-z]+}">(line); \/\/ Listă, sumă de tipuri, formă încorporată. 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"); \/\/ Începutul inputului și ceea ce a rămas de la el. const auto [value, rest] = scan::scan_prefix<"{},{}">(line).take<point>(); \/\/ Pe înregistrare câte una, din orice. for (const row& one : scan::each<"{},{[a-z]+}\\n">(text).of<row>()) { … }
Intervalul care poate fi citit o singură dată este citit fără buffer, câmpurile fiind adunate pe măsură ce simbolurile sosesc. Câte simboluri va trebui să rețină, șablonul o stabilește în timpul compilării; acolo unde nu există un astfel de număr, citirea este respinsă în timpul compilării. 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>()) { … } Performanță.
Măsurătorile au fost efectuate pe Ryzen 9 9950X, clang 22.1.8 cu libc++, -O3 -march=native, LTO. Treizeci și două de înregistrări pe rulare, mediana celor șapte runde. «([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 aceleași cinci câmpuri, câte două sute de litere fiecare scan::scan<f>.sentinel() 57.8 ns re2c 637 ns CTRE 692 ns RE2 11751 ns «first.last@subdomain.example.com» recunoaștere, nimic nu este extras scan::match<p>.sentinel().scalar() 436 ns scan::match<p>.scalar() 545 ns re2c 1186 ns RE2 2616 ns CTRE 14339 ns Comprimarea grupurilor pe parcursul analizei
Tipul este informat despre care dintre grupurile sale aparține caracterul curent, iar calculul se efectuează pe loc: nicio iterație a buclei nu este salvată, nicio subsecvență nu este creată. 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»
Orice obiect al apelantului — alocatoare, pool, arenă, orice altceva — poate fi transmis în acel apel care generează valoarea. Tipul contextului nu se pierde: starea scanner-ului și fiecare hook pot fi template-uri pentru acesta, astfel încât în scanner nu este menționat niciun tip de apelant. 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); // unul pentru ambele locuri scan::scan(text).of(mine, scan::default_context); // câte unul pentru fiecare loc scan::scan(text).of({mine, scan::default_context}); // același lucru în paranteze const both got = scan::scan(text).with(mine); // Contextul nu trebuie să fie propriul său tip: alocatoarele ajung până la a construi citirea. struct two { std::pmr::string name; std::pmr::string tail; }; const two kept = scan::scan(text).of<( std::pmr::polymorphic_allocator(&bytes));
Hook-ul primește obiectul apelant, nu o copie, așa că starea poate păstra adresa sa: contextul trăiește la fel de mult ca apelul, iar întreaga citire se desfășoară în interiorul acestui apel. Alte caracteristici
- Două straturi: strat de template-uri (match, starts_with, search, search_all, split) și strat de formate, unde „{}” este un câmp, iar valoarea locului este determinată de câmpurile tipului însuși.
- Regula de rezolvare a ambiguității este leftmost-first, ca în Perl, RE2 și CTRE.
- Toate cele menționate funcționează și în expresii constante.
- Nu există lookaround, referințe inverse și proprietăți Unicode; template-urile funcționează cu bytes.
- Template-urile cunoscute doar la rulare nu sunt acceptate.
Compilare
Se compilează în clang și GCC. Modulele C++ nu sunt obligatorii: sunt disponibile fișierele de antet (include/), generate din aceleași module de utilitarul demodulizer — fiecare push în main recompune, leagă și le comite din nou. Singura dependență, Boost.PFR, nu este necesară când packs-urile de binding din C++26 sunt activate. 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)
Sursa: opennet.ro
