Die erste Ausgabe der scan-Bibliothek (0.1.0) wurde veröffentlicht, die Text in Werte entsprechend einem zur Kompilierungszeit bekannten Muster analysiert. Das Muster oder Format wird als Template-Argument in C++ übergeben und wird zur Kompilierungszeit in einen gekennzeichneten deterministischen Endzustand-Automaten (tagged deterministic finite automaton, TDFA) umgewandelt, dessen Durchlauf zu Kompilierungszeit mit Hilfe von C++-Templates in Code umgesetzt wird. Das Ergebnis der Ausführung ist ein Wert des angeforderten Typs. Der Projektcode ist in C++23 geschrieben und steht unter der Lizenz GPLv3. import scan; // Entspricht die gesamte Zeile. Jedes Eingangssignal ist zudem ein Bereichsadapter. scan::match(address); address | scan::match; // Mit Gruppen. const auto found = scan::match("42-abc"); found.get().to_view(); // "42" // Anfang der Zeile, belegt durch das Muster, und erstes Übereinstimmen wo auch immer. scan::starts_with("abc123").whole().to_view(); // "abc" scan::search("id=4210x").to_view(); // "4210" // Alle Übereinstimmungen und Stücke dazwischen — faule Darstellungen. for (const auto& one : text | scan::search_all) { … } const auto fields = "a,bb,,ccc" | scan::split | std::ranges::to(); // Werte, nicht Text. struct row { int id; std::string_view name; }; const row one = scan::scan(line); // Liste, Summe von Typen, verschachtelte Form. struct all { std::vector values; std::variant tail; }; const all got = scan::scan("1,2,3 abc"); // Beginn der Eingabe und das, was von ihr übrig bleibt. const auto [value, rest] = scan::scan_prefix(line).take(); // Nach der Aufnahme einmal, aus was auch immer. for (const row& one : scan::each(text).of()) { … }
Der Bereich, der nur einmal gelesen werden kann, wird ohne Pufferung gelesen, Felder werden während des Eingangs von Zeichen gesammelt. Wie viele Zeichen gehalten werden müssen, legt das Muster zur Kompilierungszeit fest; wo eine solche Anzahl nicht vorhanden ist, wird das Lesen zur Kompilierungszeit abgelehnt. 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()) { … } Leistung.
Die Messungen wurden auf einem Ryzen 9 9950X, clang 22.1.8 mit libc++, -O3 -march=native, LTO durchgeführt. Zweiunddreißig Aufzeichnungen pro Durchgang, Median von sieben Durchgängen. «([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 dieselben fünf Felder, jeweils zweihundert Buchstaben scan::scan<f>.sentinel() 57.8 ns re2c 637 ns CTRE 692 ns RE2 11751 ns «first.last@subdomain.example.com» Erkennung, es wird nichts extrahiert scan::match<p>.sentinel().scalar() 436 ns scan::match<p>.scalar() 545 ns re2c 1186 ns RE2 2616 ns CTRE 14339 ns Zusammenfassung von Gruppen während der Analyse
Der Typ erhält die Zugehörigkeit des nächsten Zeichens zu einer seiner Gruppen, und die Berechnung erfolgt im Vorfeld: keine Iteration der Schleife wird gespeichert, es wird keine Teilzeichenkette erzeugt. 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={}{[a-z]*}»(«value=(__X_XX)abcdefgh»).of(); // got.number.value == 12, got.tail == «abcdefgh»
Jedes Objekt der aufrufenden Seite — Allokator, Pool, Arena, was auch immer — kann an den Aufruf übergeben werden, der den Wert erstellt. Der Typ des Kontexts bleibt dabei erhalten: Der Zustand des Scanners und jeder seiner Hooks können Vorlagen gemäß diesem Typ sein, sodass im Scanner kein Typ der aufrufenden Seite erwähnt wird. 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); // einer für beide Plätze scan::scan(text).of(mine, scan::default_context); // einer pro Platz scan::scan(text).of({mine, scan::default_context}); // das gleiche in Klammern const both got = scan::scan(text).with(mine); // Der Kontext muss nicht unbedingt seinen Typ haben: Der Allokator geht so weit, // dass er das Lesen aufbaut. struct two { std::pmr::string name; std::pmr::string tail; }; const two kept = scan::scan(text).of<( std::pmr::polymorphic_allocator(&bytes));
Der Hook erhält das Objekt der aufrufenden Seite und nicht eine Kopie, sodass der Zustand seine Adresse speichern kann: Der Kontext lebt so lange wie der Aufruf, und das gesamte Lesen findet innerhalb dieses Aufrufs statt. Weitere Merkmale
- Zwei Schichten: die Schicht der Vorlagen (match, starts_with, search, search_all, split) und die Schicht der Formate, wobei "{}" das Feld ist und die Werte des Ortes durch die Felder des Typs selbst bestimmt werden.
- Die Regel zur Auflösung von Mehrdeutigkeiten ist leftmost-first, wie in Perl, RE2 und CTRE.
- Alles Aufgezählte funktioniert auch in konstanten Ausdrücken.
- Es gibt kein Lookaround, keine Rückverweise und keine Unicode-Eigenschaften; Vorlagen arbeiten mit Bytes.
- Vorlagen, die nur zur Laufzeit bekannt sind, werden nicht unterstützt.
Bau
Kompatibel mit Clang und GCC. C++-Module sind nicht erforderlich: Es stehen Header-Dateien (include/) zur Verfügung, die von dem gleichen Modul-Tool, dem Demodulisierer, erzeugt werden - jeder Push in Main führt zu einer Neuübersetzung, Verlinkung und Rückcommitierung. Die einzige Abhängigkeit, Boost.PFR, ist nicht erforderlich, wenn die Binding-Packs aus C++26 aktiviert sind. 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)
Quelle: opennet.ru
