Le premier numéro de la bibliothèque scan (0.1.0) a été publié, analysant le texte en valeurs selon un schéma connu au moment de la compilation. Le schéma ou le format est inscrit comme argument de modèle C++ et est transformé en un automate fini déterministe annoté (tagged deterministic finite automaton, TDFA) lors de la compilation. Le parcours de cet automate se déplie en états dans le code au moment de la compilation en utilisant des modèles C++. Le résultat de l'exécution est une valeur du type demandé. Le code du projet est écrit en C++23 et est distribué sous la licence GPLv3. import scan; // Vérifie si toute la chaîne correspond. Chaque point d'entrée est également un adaptateur de plage. scan::match(address); address | scan::match; // Avec des groupes. const auto found = scan::match("42-abc"); found.get().to_view(); // "42" // Début de la chaîne, occupé par le modèle, et première correspondance n'importe où. scan::starts_with("abc123").whole().to_view(); // "abc" scan::search("id=4210x").to_view(); // "4210" // Toutes les correspondances et morceaux entre elles - des représentations paresseuses. for (const auto& one : text | scan::search_all) { ... } const auto fields = "a,bb,,ccc" | scan::split() | std::ranges::to(); // Valeurs, pas texte. struct row { int id; std::string_view name; }; const row one = scan::scan(line); // Liste, somme des types, forme imbriquée. struct all { std::vector values; std::variant tail; }; const all got = scan::scan("1,2,3 abc"); // Début d'entrée et ce qui reste. const auto [value, rest] = scan::scan_prefix(line).take(); // Par enregistrement à la fois, de n'importe quoi. for (const row& one : scan::each(text).of()) { ... }
Une plage qui ne peut être lue qu'une seule fois est lue sans tampon ; les champs sont rassemblés au fur et à mesure que les caractères arrivent. Combien de caractères seront conservés, le modèle le précise lors de la compilation ; là où il n'y a pas de nombre, la lecture est rejetée lors de la compilation. 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()) { ... } Performance.
Les mesures ont été effectuées sur un Ryzen 9 9950X, clang 22.1.8 avec libc++, -O3 -march=native,LTO. Trente-deux enregistrements par passage, médiane de sept passages. «([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 les mêmes cinq champs, deux cents lettres chacun scan::scan<f>.sentinel() 57.8 ns re2c 637 ns CTRE 692 ns RE2 11751 ns «first.last@subdomain.example.com» reconnaissance, rien n'est extrait scan::match<p>.sentinel().scalar() 436 ns scan::match<p>.scalar() 545 ns re2c 1186 ns RE2 2616 ns CTRE 14339 ns Regroupement des groupes lors de l'analyse.
Le type reçoit des informations sur à quel groupe il appartient pour chaque symbole, et le calcul est effectué sur place : aucune itération de boucle n'est conservée, aucune sous-chaîne n'est créée. 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<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»
Tout objet de l'appelant — un allocateur, un pool, une arène, n'importe quoi d'autre — peut être transmis à cet appel qui crée une valeur. Le type de contexte n'est pas perdu : l'état du scanner et chacun de ses hooks peuvent être des modèles en fonction de celui-ci, de sorte qu'aucun type d'appelant n'est mentionné dans le 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); // un pour les deux lieux scan::scan(text).of(mine, scan::default_context); // un par lieu scan::scan(text).of({mine, scan::default_context}); // même chose dans les parenthèses const both got = scan::scan(text).with(mine); // Le contexte n'a pas nécessairement à être de son propre type : l'allocateur va jusqu'à // construire la lecture. struct two { std::pmr::string name; std::pmr::string tail; }; const two kept = scan::scan(text).of<( std::pmr::polymorphic_allocator (&bytes));
Le hook reçoit l'objet de l'appelant lui-même, et non une copie, donc l'état peut stocker son adresse : le contexte vit aussi longtemps que l'appel, et toute la lecture se fait à l'intérieur de cet appel. Autres caractéristiques
- Deux couches : une couche de modèles (match, starts_with, search, search_all, split) et une couche de formats, où «{}» est un champ et la valeur de l'emplacement est définie par les champs du type lui-même.
- La règle de résolution de l'ambiguïté est leftmost-first, comme dans Perl, RE2 et CTRE.
- Tout ce qui est énuméré fonctionne également dans des expressions constantes.
- Pas de lookaround, de références inverses et de propriétés Unicode ; les modèles fonctionnent avec des octets.
- Les modèles connus uniquement à l'exécution ne sont pas pris en charge.
Assemblage
S'assemble avec clang et GCC. Les modules C++ ne sont pas obligatoires : des fichiers d'en-tête (include/) générés à partir des mêmes modules par l'utilitaire demodulizer sont disponibles — chaque push dans main reconstruit, lie et les commite à nouveau. La seule dépendance, Boost.PFR, n'est pas nécessaire avec les packs de liaison activés depuis 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)
Source : opennet.ru
