CppCon
AI-assisted summary of CppCon 2018: Hana Dusíková “Compile Time Regular Expressions” — original source. Check the original for full context.
Source published:
Key ideas
- Compile Time Regular Expressions (CTRE) utilizes C++20 Class Non-Type Template Parameters (NTTPs) to parse regular expression strings and construct an Abstract Syntax Tree purely at compile time, eliminating runtime parsing overhead. (5:20)
- In large-scale benchmarks matching against a ~2 GB CSV file, CTRE completed execution in 3.9 seconds, outperforming egrep (50 s), GCC libstdc++ std::regex (55 s), and Clang libc++ std::regex (21 minutes). (37:53)
- CTRE compiles regular expressions in linear time O(N) relative to pattern length, adding roughly 200 ms per unique pattern on modern hardware while generating as few as 24 to 98 lines of optimized assembly compared to ~20,000 lines linked by std::regex. (43:03)
- Because all matching functions are constexpr and completely avoid dynamic heap allocations, the exact same functional traversal can validate strings at compile time or compile into native machine code for runtime std::string_view evaluation. (50:05)
Detailed notes
## Background Published on YouTube by CppCon on October 24, 2018, this presentation features Hana Dusíková demonstrating her Compile Time Regular Expressions (CTRE) library. The session covers how C++20 features allow regular expressions to be constructed directly from compile-time strings, eliminating the runtime parsing overhead and linked-library dependencies inherent in traditional regex engines while comparing generated code against existing solutions in Compiler Explorer.
## TL;DR Hana Dusíková introduces CTRE, an open-source library that parses regular expression strings at compile time using an internal LL(1) parser driven by C++ function overloading and Class Non-Type Template Parameters. By shifting pattern validation and Abstract Syntax Tree (AST) construction entirely into constexpr template metaprogramming, the engine generates compact machine code and guarantees linear compile-time parsing complexity without runtime constructor costs. Real-world benchmarks against std::regex and PCRE2 demonstrate dramatic runtime speedups on large datasets while avoiding template error cascades and binary bloat. Dusíková concludes with live Compiler Explorer demonstrations, explaining how a purely functional, heap-free matching pipeline enables identical code to validate strings at compile time or compile into tightly optimized assembly for runtime evaluation.
## Atmosphere & style The presentation carries an engaging, approachable, and self-deprecating tone, characterized by Dusíková's deadpan humor about slide generation bugs, fear of Flex and Bison, and playful jabs at standard library performance. The room reacts with genuine enthusiasm, bursting into spontaneous applause and laughter at extreme runtime contrasts—especially libc++'s multi-minute execution times. Despite tackling intricate template metaprogramming concepts and formal language theory, the dialogue between Dusíková and attendees like Bryce Adelstein Lelbach and Odin Holmes feels warm, interactive, and deeply collegial.
## Bulletin - [05:20](https://www.youtube.com/watch?v=QM3W36COnE4&t=320s) Compile Time Regular Expressions (CTRE) utilizes C++20 Class Non-Type Template Parameters (NTTPs) to parse regular expression strings and construct an Abstract Syntax Tree purely at compile time, eliminating runtime parsing overhead. - [37:53](https://www.youtube.com/watch?v=QM3W36COnE4&t=2273s) In large-scale benchmarks matching against a ~2 GB CSV file, CTRE completed execution in 3.9 seconds, outperforming egrep (50 s), GCC libstdc++ std::regex (55 s), and Clang libc++ std::regex (21 minutes). - [43:03](https://www.youtube.com/watch?v=QM3W36COnE4&t=2583s) CTRE compiles regular expressions in linear time O(N) relative to pattern length, adding roughly 200 ms per unique pattern on modern hardware while generating as few as 24 to 98 lines of optimized assembly compared to ~20,000 lines linked by std::regex. - [50:05](https://www.youtube.com/watch?v=QM3W36COnE4&t=3005s) Because all matching functions are constexpr and completely avoid dynamic heap allocations, the exact same functional traversal can validate strings at compile time or compile into native machine code for runtime std::string_view evaluation.
## Key takeaways - [00:00](https://www.youtube.com/watch?v=QM3W36COnE4&t=0s) Traditional regular expression engines like std::regex impose severe runtime penalties by parsing pattern strings and building automata at runtime, an exponential complexity process that should be calculated via constexpr instead. - [00:00](https://www.youtube.com/watch?v=QM3W36COnE4&t=0s) std::regex lacks native support for std::string_view, forcing developers into cumbersome iterator pair syntax. - [05:20](https://www.youtube.com/watch?v=QM3W36COnE4&t=320s) C++20 Class Non-Type Template Parameters (NTTPs) allow fixed literal string wrappers to be passed directly as template arguments, enabling compile-time regex pattern definitions. - [05:20](https://www.youtube.com/watch?v=QM3W36COnE4&t=320s) Invalid regular expression syntax triggers clean, concise static_assert failure messages at compile time rather than massive template error cascades. - [05:20](https://www.youtube.com/watch?v=QM3W36COnE4&t=320s) CTRE generates exceptionally lean assembly (~98 lines for CSV extraction and ~24 lines for literal matching), avoiding the roughly 20,000 lines of runtime engine assembly required by std::regex. - [11:20](https://www.youtube.com/watch?v=QM3W36COnE4&t=680s) The pattern parsing engine is built as an LL(1) table-driven parser that operates at compile time using purely functional transformations and function overloading as the transition function. - [14:49](https://www.youtube.com/watch?v=QM3W36COnE4&t=889s) By completely decoupling the generic LL(1) parser algorithm from the grammar definition struct, the same engine architecture can theoretically parse other grammars like JSON or Brainfuck. - [18:04](https://www.youtube.com/watch?v=QM3W36COnE4&t=1084s) Specialized variants of the LL(1) grammar allow the compile-time parser to operate in linear O(N) time with zero backtracking during pattern parsing. - [22:38](https://www.youtube.com/watch?v=QM3W36COnE4&t=1358s) Semantic action markers embedded within grammar rules transform the parse tree into an Abstract Syntax Tree (AST) expression template using functional modify() template overloads. - [31:29](https://www.youtube.com/watch?v=QM3W36COnE4&t=1889s) The runtime matching engine executes recursive matching across stack-based type lists without runtime abstraction overhead. - [37:53](https://www.youtube.com/watch?v=QM3W36COnE4&t=2273s) While Deterministic Finite Automata (DFAs) provide rapid matching, they struggle with tracking submatch boundaries, necessitating backtracking non-deterministic finite automata (NFAs) for full capture support. - [43:03](https://www.youtube.com/watch?v=QM3W36COnE4&t=2583s) Counterintuitively, compiling translation units with CTRE is often faster than compiling std::regex headers because the standard library's heavy runtime engine strains compiler optimization passes. - [43:03](https://www.youtube.com/watch?v=QM3W36COnE4&t=2583s) Duplicate regex instantiations across a translation unit incur virtually zero additional compile time due to compiler template instantiation caching. - [48:56](https://www.youtube.com/watch?v=QM3W36COnE4&t=2936s) Practical regular expression lengths are currently constrained to around 1,000 characters by compiler recursion depth limits, though input grammar complexity itself does not degrade parse performance. - [46:53](https://www.youtube.com/watch?v=QM3W36COnE4&t=2813s) Hana Dusíková advocates that modern modernizations should eventually be merged directly into standard C++ so that std::regex natively evaluates regular expressions at compile time.
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=QM3W36COnE4&t=0s) Introduction and Motivation for Compile-Time Regular Expressions
Hana Dusíková opens the talk welcoming the audience, joking that if anyone wants to take her place on stage they are welcome to do so. She introduces herself as a senior researcher at Avast living in Brno, Czech Republic (Czechia), where she organizes the local Avast C++ meetup. She notes that she will soon represent the Czech national body for WG21, the ISO C++ standards committee. She also quips that while she is an amateur photographer, as of the previous week she decided to stop hiking, adding with a smile, “Bryce will tell you more.”
Turning to the core subject, regular expressions serve three main tasks across programming: matching an input subject against a pattern, extracting matching sub-content, or modifying parts of the subject. Matching alone can be executed using a deterministic finite automaton (DFA), which runs significantly faster than the backtracking or search-based approaches required for capturing content. Basic regular expressions (such as `[a-c0-9]+`) vary across dialects like ECMAScript, Perl, and PCRE in advanced capabilities, but their fundamental mechanics remain identical. In standard C++, using regular expressions requires instantiating an engine object with a pattern and invoking an algorithm like `std::regex_match`:
```cpp std::regex r{"[a-c0-9]+"}; std::regex_match(str.begin(), str.end(), r); ```
Dusíková points out an immediate annoyance: because `std::regex` lacks direct support for `std::string_view`, developers are forced to pass iterator pairs. The fundamental pipeline in C++ and C is identical across almost every language: define a pattern string, pass it to a constructor to build the engine, and execute matches against inputs.
```mermaid flowchart LR accTitle: Traditional Regex Lifecycle accDescr: Pipeline showing pattern parsing and automaton building at runtime followed by matching. subgraph S1["Runtime Constructor"] A["Pattern String"] --> B["Parser"] B --> C["Automaton / Tree"] end subgraph S2["Runtime Execution"] C --> D["Match Input Subject"] end ```
Building and matching within the same hot function must be avoided at all costs due to severe performance penalties. The constructor carries the burden of parsing the pattern string and constructing complex internal representations—whether trees, lookup tables, or finite state machines. Dusíková emphasizes that building a deterministic finite automaton is an exponential-complexity algorithm.
To avoid repeated constructor invocations, engineers traditionally write hand-rolled state machines in several hundred lines of code, but this is tedious, error-prone, and difficult to maintain. Alternatively, third-party code generators like Flex and Bison introduce heavy build pipeline complexity and portability headaches across target platforms. While interpreted languages like Perl provide built-in language-level support, C++ has historically lacked native regular expressions. Dusíková argues that running the parser and constructor dynamically makes no sense when the pattern is fixed:
> “So, if you have application installed on million of mobile phones, each mobile phone, every time, do the same calculation. Why? It should be constexpr. It should be pre-calculated.”
Developers should treat regular expressions just like compiled C++ code: “No one will use interpret of Python in their code to match string, but we are doing it with regular expressions.” An ideal implementation demands zero runtime cost for parsing the pattern, absolute maximum throughput, and zero overhead.
---
[05:20](https://www.youtube.com/watch?v=QM3W36COnE4&t=320s) C++20 Class NTTPs, CTRE Syntax, and Assembly Output
Dusíková presents the modern interface made possible by the Compile Time Regular Expression (CTRE) library. Users express patterns directly inside template arguments alongside the subject string:
```cpp ctre::match<"[a-c0-9]+">(subject); ```
This syntax is readable, easy to maintain, and backed by elegant mechanics. In C++20, this design leverages Class Non-Type Template Parameters (Class NTTPs), allowing class types to serve as template arguments. While the core language feature was merged into the C++ working draft, compilers did not yet fully implement standard library support at the time. To bridge this gap, Dusíková relies on the N3599 extension (`template <typename Char, Char... Cs> operator""_ctre()`) authored by Richard Smith, supported in GCC since version 4 and in Clang.
The compile-time approach unlocks expressive patterns and ironclad static verification: - **Compile-Time Testing:** Expressions can be evaluated directly in `constexpr` contexts; if an expression fails to match test data, the binary simply refuses to compile. - **Structured Data Extraction:** Capturing first and last names from CSV inputs requires only reading match captures cleanly (e.g., `match.get<1>()`, `match.get<2>()`). - **Structured Bindings Support:** Date extraction directly integrates with C++17 structured bindings (`auto [all, year, month, day] = ctre::match<"...">(input);`).
Anticipating skepticism that templates will emit bloated assembly, Dusíková shares concrete compiler benchmarks. Compiling the CSV first-and-last-name extraction with CTRE generates a lean, tightly optimized 98 lines of assembly. In contrast, while compiler optimizers should theoretically recognize intent and collapse `std::regex`, the standard library alternative generates nearly 20,000 lines of assembly because the entire runtime engine runtime apparatus is linked into the binary.
```mermaid flowchart TD accTitle: CTRE Error Handling and Assembly Comparison accDescr: Contrasting CTRE's compact assembly and static errors with std::regex bloating. subgraph CTRE["Compile-Time Regular Expressions (CTRE)"] direction TB C1["constexpr Pattern Parsing"] --> C2["98 lines of clean assembly"] C1 --> C3["Concise 2-line compile error on invalid syntax"] end subgraph STD["std::regex (Runtime)"] direction TB S1["Runtime Construction"] --> S2["~20,000 lines of assembly included"] end ```
When audience members ask whether malformed regexes cause the notorious template error cascades of C++03, Dusíková demonstrates that invalid syntax triggers a clean two-line error highlighting `regular expression syntax error` via `static_assert`. An audience member asks if all errors remain that concise; Dusíková clarifies that every syntax error detected by the parser is trapped in a `static_assert`.
Addressing questions about literal escapes, she confirms that users can utilize raw string literals to avoid double-backslash escaping (`R"(\d+)"`). When another attendee asks about MSVC support, she explains that while the original string literal operator template extension paper was rejected, the adoption of Class NTTPs in C++20 guarantees vendor support across all conforming toolchains going forward. CTRE targets full PCRE compatibility, making input validation cheap, maintainable, and blistering fast.
---
[11:20](https://www.youtube.com/watch?v=QM3W36COnE4&t=680s) Inside the Engine: Generic LL(1) Parser Architecture
Diving into the mechanics of transforming strings into C++ types, Dusíková explains:
> “I'm using generic LL(1) parser, which I write, to convert pattern of a string into a type.”
The parser accepts a PCRE-compatible grammar as a parameter, making the underlying engine adaptable to alternative regex dialects. The parser's output type encodes the pattern into an expression template (without evaluating expressions directly), which is subsequently evaluated against the input subject.
An LL(1) parser operates deterministically using an internal symbol stack: 1. It initializes with a distinct start symbol pushed onto the stack. 2. At every iteration, it inspects the top stack symbol alongside the current lookahead character from the input stream. 3. Based on the `(stack_symbol, input_character)` tuple in the parsing table, the parser executes one of four transitions: - Push a replacement sequence of symbols onto the stack. - Pop a symbol or push an empty string ($\epsilon$-transition). - Consume (pop) the matched character from the input and advance. - Reject the input immediately if no valid transition cell exists. 4. If the stack is empty exactly when the input stream is fully consumed, the parser successfully accepts the input.
Dusíková explains the challenge of implementing this formal table-driven automaton inside C++ templates where dynamic heap allocations are impossible. Having rewritten the library six distinct times, she restarted from scratch specifically while preparing this presentation and discovered an even cleaner implementation strategy.
The symbols manipulated on the parser stack are represented entirely as empty tag structs:
```cpp struct start_symbol {}; struct symbol_a {}; struct symbol_b {}; ```
Because these structs carry no runtime data or state, the entire compile-time parser operates purely through types and template metaprogramming transformations.
[14:49](https://www.youtube.com/watch?v=QM3W36COnE4&t=889s) Representing the LL(1) Table in Modern C++
```mermaid flowchart TD accTitle: LL(1) Parser Step Architecture accDescr: Decision flow showing how symbol matching, stack pushing, popping, accepting, and rejecting work via overload resolution.
subgraph InputState ["Input State"] InChar["Current Character"] StackSym["Top Stack Symbol"] end
subgraph Resolution ["Overload Resolution"] Match{"Compare Stack & Input"} Match -->|"Mismatch / Transition Rule"| Push["Push Symbols to Stack"] Match -->|"Same Symbol"| Pop["Pop from Stack & Advance Input"] Match -->|"No Matching Rule"| Reject["Reject (false)"] Match -->|"Empty Stack & Empty Input"| Accept["Accept (true)"] end
InputState --> Resolution ```
Dusíková explains how to encode an LL(1) parse table into C++. Rather than relying on massive run-time state tables, she turns to a foundational language mechanism: “And then, I will use the most modern C plus plus features. Function or overloading.” Overload resolution acts as the transition function for the automaton. If a rule specifies a non-terminal symbol and a particular lookahead character, the function overload returns a list of symbols to push onto the parsing stack. If the symbol on the top of the stack matches the input symbol, it pops the symbol and consumes the character. If no valid transition exists in the table, it rejects; and if both the stack and the input stream are simultaneously empty, the parser accepts. Because the logic operates strictly at the type level to drive the automaton, the functions do not require executable runtime bodies: “I'm sure this function doesn't need anybody 'cause it just need to know the pattern type, nothing else.”
The grammar itself is encapsulated in a dedicated `struct` passed as a template parameter to the parser algorithm. This architectural decoupling makes the parser engine generic. By supplying an alternate grammar definition, the exact same compile-time engine can parse JSON, regular expressions, or even esoteric languages like Brainfuck. A member of the audience asks whether the grammar supports backreferences. Dusíková clarifies the distinction between the stages: “We are talking about parsing the pattern, not yet about regular expression.” She reassures the audience that backreferences are indeed supported later by the library.
- An LL(1) parser table is encoded purely through C++ function overloading, relying on return types to decide whether to push, pop, accept, or reject. - The parse stack is represented using a compile-time `type_list` structure with functional operations: `push`, `pop`, and `top` (which returns an explicit empty stack symbol when exhausted). - Designing compile-time code requires shifting away from procedural idioms: “Compile-time programming starts very easy if you think functionally. You should forget about using variables, cycles, everything is a conversion and different function eroding.”
---
[18:04](https://www.youtube.com/watch?v=QM3W36COnE4&t=1084s) Handling Compile-Time String Inputs with Fixed String
To feed a literal regular expression into the compile-time parser, Dusíková relies on the `fixed_string` proposal championed by Jeff Snyder, who is present in the audience. Passing string literals into templates had historically been notoriously cumbersome, requiring macro workarounds or unpacking characters into variadic packs (`char_pack`). Using Snyder's technique, a `fixed_string` stores its contents directly inside an internal member array. This approach is substantially faster to compile than older compiler-specific extension hacks.
This mechanism is enabled by C++20 features: class types in non-type template parameters (NTTP), combined with template argument deduction placeholders. Instead of being restricted to integral values, pointers, or references, developers can use a literal class value directly as a template argument. Dusíková notes that `std::fixed_string` (or equivalent structural string wrappers) will likely become a standardized part of C++20.
```mermaid flowchart LR accTitle: Extracting Characters from Fixed String accDescr: Character retrieval checking bounds to return either a Terminal symbol or Epsilon.
Request["Character Request at Index N"] --> Check{"Index N < String Size?"} Check -->|"Yes"| Term["Return Terminal<char>"] Check -->|"No"| Eps["Return Epsilon"] ```
To drive the LL(1) parser, the implementation queries characters by index from the `fixed_string`: - If the requested position is within the bounds of the string length, the accessor returns a `terminal<char>` type wrapping that character. - If the index reaches or exceeds the string length, it returns an `epsilon` symbol, signaling the end of the input stream. - Using this specialized LL(1) formulation, parsing achieves linear complexity: “And actually, because I'm using special variant of LL one grammar, it's O N calculation.”
---
[20:27](https://www.youtube.com/watch?v=QM3W36COnE4&t=1227s) The Recursive Compile-Time Parsing Loop
Dusíková walks through the implementation of the recursive parser. The parser function is structured intentionally with verbose type aliases to illustrate that every stage evaluates strictly at compile time: “Everything is type here.” The entry-point function initiates parsing by taking a stack primed with the start symbol `S` along with the input `fixed_string`. At each step, it inspects the top symbol of the stack and the current input character, and delegates to the grammar overloads to determine the next action.
```mermaid flowchart TD accTitle: Compile-Time Move Function Transitions accDescr: Recursive move logic dispatching to push symbols, advance input, reject, or accept.
Move["parse::move(...)"] --> Action{"Table Decision"} Action -->|"Push Rule"| P["Push new symbols to stack & Recurse"] Action -->|"Terminal Match"| A["Pop stack, Advance index & Recurse"] Action -->|"Reject State"| R["Return false"] Action -->|"Accept State"| Acc["Return true"] ```
The transition logic is encapsulated inside a `move` helper function. When a rule specifies pushing symbols, `move` prepends the new types to the remaining stack and recurses. When a terminal matches the current input, it advances the string index by one character and pops the stack. If a reject state is encountered, it returns `false`; upon reaching the accept state, it returns `true`.
An audience member asks about compiler failure modes on invalid or excessively long inputs, and whether compilation times explode. Dusíková explains that if an input exceeds compiler recursion thresholds, it will stop around the compiler's depth limit (typically around 1,000 characters). However, for practical regular expressions, which are usually short (at most around 200 characters), this is well within acceptable boundaries. Because the grammar pops an input symbol at each step without backtracking, compile times do not blow up: “No, it's O N actually because it's a linear length of input.”
---
[22:38](https://www.youtube.com/watch?v=QM3W36COnE4&t=1358s) Generating Expression Templates via Semantic Actions
A parser that merely yields a compile-time `true` or `false` validates syntax, but it cannot execute a match. To generate an executable regular expression engine, the parser must construct an Abstract Syntax Tree (AST) in the form of an expression template: “We need to build an expression template from our parser. So, we need to somehow modify the parser to give some input other than just boolean.”
```mermaid flowchart TD accTitle: AST Construction via Semantic Actions accDescr: How parser encounters semantic action markers on the stack and modifies the subject AST.
Token["Parse Token / Hit Action Symbol"] --> CheckSym{"Is Top Symbol a Semantic Action?"} CheckSym -->|"Regular Grammar Symbol"| MoveDirect["Forward Subject Unmodified to move()"] CheckSym -->|"Semantic Action Symbol"| Mod["Call modify(symbol, subject)"] Mod --> NewSubj["Produce New Subject AST Type"] NewSubj --> Forward["Pass New Subject to Parser Continuation"] ```
To achieve this, Dusíková modifies the parser signature and the grammar: 1. **The Subject Type:** A new accumulator parameter, termed `Subject` (defaulting to an empty `type_list`), is threaded through the recursive parsing functions. 2. **Semantic Actions:** Special action symbols are embedded into the grammar production rules alongside regular non-terminals. These actions are tagged so the parser can detect them at compile time using C++20 Concepts. 3. **AST Construction:** When the parser encounters a semantic action marker on its stack, it intercepts the evaluation, pops the marker, calls a `modify(...)` function that updates the `Subject` type, and passes the resulting type downstream. 4. **Result Typing:** Instead of returning a `bool`, the modified parser's return type is deduced as `auto`, returning a composite result holding both a success flag and the fully constructed AST type.
---
[25:30](https://www.youtube.com/watch?v=QM3W36COnE4&t=1530s) Live AST Tree Transformations
Dusíková demonstrates the parse tree and its corresponding semantic actions using an interactive visual presentation. In the parse tree, nodes represent symbols on the stack expanding downward from the start symbol `S`. At the leaves, distinctive colored markers represent semantic actions.
When the parser encounters a character action, it pushes a character node into the AST stack. Subsequent characters are pushed onto the stack in sequence. When an operator such as a `+` (plus) is processed, it does not perform arithmetic: “In this case, it's not operation like numerical plus, it's take one char from stack and it wrap it into plus.” Similarly, sequence actions take the top two elements from the stack and wrap them into a `sequence<A, B>` node.
An audience member asks whether the AST shown on the slides corresponds to the raw de-mangled C++ type: > - **Audience Member:** “Yeah, so this is the thought of it. If we were to de-mangle the type, this is what we would actually see?” > - **Hana Dusíková:** “Yeah.”
Dusíková demonstrates live adjustments to regular expression strings on the slide, showing how the rendered output type dynamically adapts to inputs like `a+` or optionals. She tests playful examples like `(Jeff is best)?`, eliciting laughter from the audience. When an audience member notices that space characters do not display properly in a demo string, Dusíková jokes about slide generation bugs: > “Question was how it works with blank character. It's my mistake. I forgot to add quotes here. It's generated by JavaScript in this case. Library is right in C plus plus, the slides are not.”
---
[27:42](https://www.youtube.com/watch?v=QM3W36COnE4&t=1662s) Implementation of the Modify Transformations
The AST transformation functions (`modify`) are purely functional type-level transformations without runtime bodies. Dusíková walks through the signatures that construct each regex node:
- **Character Push:** Takes the current `Subject` type list and prepends a `char_tag<C>`: “Notice again, there is no body, just signature of a function. It's functional.” - **Optional (`?`):** Pops the top item from the stack, wraps it into an `optional<T>` template, and places it back onto the stack. - **Cycles/Repetitions (`+`, `*`):** Takes the top element, wraps it into a repetition node, and pushes it back. - **Sequences:** Pops two adjacent elements off the stack and packages them into a `sequence<A, B>`. Because stack operations reverse order, the function accounts for operand order: “Notice they are in another order because on stack they are in there. Only this one is at the top.” - **Extended Sequences & Alternations:** When additional elements are joined into existing sequences or alternations (`A | B`), overloaded variants take the existing composite node, append the new item, and return the combined type: “It's functional programming with C plus plus syntax.” - **Character Classes:** Specific actions map shorthand sequences like `\d` or `\w` directly to predefined symbol classes, such as `digit_class` or `alpha_class`.
Once parsing concludes, the AST output type is completely known to the compiler. It can be validated using static assertions (`static_assert`) to ensure that the pattern compiles to the exact expected AST type before transitioning to execution. Dusíková then introduces the next phase of the pipeline: taking this parsed expression template and evaluating it against runtime target strings using the `regex_match` function.
[31:29](https://www.youtube.com/watch?v=QM3W36COnE4&t=1889s) The Matching Engine and Stack-Based Type Lists
Hana Dusíková explains the bridge between the compile-time parser and the actual runtime evaluation. Once the regular expression pattern string has been verified using `static_assert`, the expression template tree is extracted directly from the parser as clean, fully evaluated types. The compiler has finished computing the structure, leaving no leftover parsing overhead for runtime. The top-level `match` function accepts the begin and end iterators of the subject string and is initialized with a stack-like list containing the parsed regular expression elements. Dusíková emphasizes the architecture of the engine: “Everything here is based on the type list.”
```mermaid flowchart TD accTitle: Compile-Time Parsing to Execution Flow accDescr: Diagram showing how a regex pattern moves from static validation to recursive stack matching.
subgraph CompileTime["Compile-Time Phase"] A["Pattern String Literal"] --> B["Parser & static_assert Validation"] B --> C["Extracted Expression Template Tree"] end
subgraph Runtime["Matching Phase"] C --> D["Top-level match(begin, end, type_list)"] D --> E{"Is type_list empty?"} E -- "Yes" --> F["Check if at end of input"] E -- "No" --> G["Pop symbol from stack & match against iterator"] G -- "Match succeeds" --> H["Advance iterator & recurse with tail"] G -- "Match fails" --> I["Backtrack or return false"] end ```
The underlying matching engine relies on a custom return type consisting of a forward iterator paired with a boolean success flag, needed to propagate intermediate positions. Dusíková walks through individual symbol handlers implemented via template function overloads: - **Empty stack terminal:** When the matching list is exhausted, the engine checks whether the current iterator has reached the end of the input; if so, it returns `true`, otherwise `false`. - **Character literals:** If the input is exhausted or the character at the current iterator does not match the expected literal, matching fails (`false`). Otherwise, it advances the iterator by one position and calls `match` on the remainder of the type list. - **Character classes (`\d`, `\w`):** Operates symmetrically to character literals by verifying that the input has not ended, checking character category membership (digits or alphabetic characters), and advancing the iterator upon a match. - **Sequences:** When a sequence node is popped from the top of the stack, the engine unwraps its inner elements, pushes them back onto the stack in order, and recurses inline without runtime abstraction penalties.
---
[34:09](https://www.youtube.com/watch?v=QM3W36COnE4&t=2049s) Alternation, Cycles, and Recursive Backtracking
Dusíková steps through the recursive rules for composite operators, beginning with optional expressions and alternations. An optional construct first attempts to match the inner content followed by the remaining stack; if that branch fails, it falls back to matching only the remainder of the stack behind the optional node. An audience member asks whether the return signature should explicitly show a `std::pair<Iterator, bool>`. Dusíková clarifies that it is an internal pair struct with implicit conversions tailored to fit on the presentation slide. For alternation (`head` | `tail`), the engine tries `head` concatenated with the trailing pattern; if that evaluation fails, it recursively evaluates the `tail` choices, returning `false` if it encounters an empty alternation branch.
The presentation then turns to repeating quantifiers (plus and star cycles). Dusíková notes a slide title mix-up between `+` and `*`, laughing as she explains the matching mechanics for unbounded loops. To implement greedy matching with backtracking, the engine evaluates the inner body of the loop followed by a special synthetic `end of cycle` marker. This marker records the iterator boundary every time an iteration completes. The engine then attempts to match the remainder of the overall pattern from that point forward; if the tail fails to match, the engine steps back to try additional iterations or terminate the cycle:
> “So depending on pattern your search can be quick or slow. So, you should think about your pattern as a code, not just write some regular expression which works. That's not what we are. We are C++ level experts, we should write great pattern.”
Dusíková points out that the simplified code shown on the slides omits captures, backreferences, and lookarounds to avoid overwhelming the audience: “I didn't want to scare you.” The full library, CTRE, implements extensive PCRE-compatible features including vector-based capture registries and lookaround assertions.
---
[37:53](https://www.youtube.com/watch?v=QM3W36COnE4&t=2273s) Runtime Benchmarks Against std::regex and PCRE2
To evaluate real-world performance, Dusíková benchmarks CTRE against `std::regex` and PCRE2 by feeding a large CSV file of almost 2 gigabytes into GCC- and Clang-compiled test harnesses.
```mermaid flowchart LR accTitle: Runtime Performance Comparison on 2GB CSV File accDescr: Bar chart representing runtime matching speed on Pattern 1.
subgraph RuntimeP1["Pattern 1 Runtime (GCC)"] CTRE["CTRE: 3.9 s"] EGREP["egrep: 50 s"] STD["std::regex (libstdc++): 55 s"] LIBCXX["std::regex (libc++): 21 min"] end ```
The initial benchmark yields striking differences: - `egrep`: 50 seconds - `std::regex` (GCC / libstdc++): 55 seconds - `CTRE`: 3.9 seconds - `std::regex` (Clang / libc++): 21 minutes
The audience breaks into applause at CTRE's speed, followed by laughter at libc++'s 21-minute run time, which Dusíková calls “embarrassing.” Marshall Clow quips from the audience, “The two samples doesn't seem consistent,” prompting Dusíková to test multiple patterns. An audience member asks whether the `std::regex_constants::optimize` flag was passed to libc++; Dusíková responds: “I tried, it didn't help.”
An audience member asks whether Dusíková considered converting patterns to a deterministic finite automaton (DFA) rather than using a backtracking non-deterministic finite automaton (NFA) / depth-first search. Dusíková confirms she has worked on DFA generation but points out the fundamental limitation: “Problem with deterministic finite state machine is that is hard to implement captures.” For patterns requiring capture groups, pure DFAs cannot easily track submatch boundaries, though hybrid approaches remain an active area of investigation. On a backtracking-heavy pattern, PCRE2 edges out CTRE slightly, while libc++ balloons to 27 minutes—and on the first test run, `std::regex` hung entirely, forcing her to kill the process.
---
[43:03](https://www.youtube.com/watch?v=QM3W36COnE4&t=2583s) Compile-Time Costs and Best Practices
Dusíková moves to compile-time benchmarks, measuring the raw time needed to compile translation units with identical header sets (excluding link time). Counterintuitively, `std::regex` takes longer to compile than CTRE under Clang, and is slower still under GCC. Suspecting that the compiler's optimization passes were getting bogged down processing `std::regex` template machinery, Dusíková disabled optimizations, but the compilation time gap persisted:
> “So, sometimes compile-time programming is even quicker than ordinary programming. It's counterintuitive, but it is.”
Bryce Adelstein Lelbach and other attendees probe how regular expression complexity scales during compilation. Dusíková shares concrete performance metrics from her MacBook: - Distinct regular expressions add approximately 200 milliseconds of compile-time overhead each (e.g., five unique expressions in a translation unit cost roughly 1.0 second). - Duplicate expressions in the same translation unit incur virtually zero additional compile time because template instantiations are cached by the compiler. - Compile-time parsing complexity scales linearly, $\mathcal{O}(N)$, with the length and complexity of the regular expression string.
Wrapping up the technical presentation, Dusíková highlights key takeaways for modern meta-programming: - Heavy reliance on recursion and overload resolution provides an expressive, highly optimizable pipeline. - Pure functional programming techniques keep compile-time logic maintainable: “You should forget about variables if you do compile-time programming because it just gives you more complexity.” - Always inspect compiler assembly to ensure the optimizer flattens recursive template abstractions into efficient machine code.
The session transitions to the audience Q&A, with an attendee asking whether compile-time and runtime regular expression engines can share a unified implementation without duplicating logic, and requesting that annotated example code be published.
[46:53](https://www.youtube.com/watch?v=QM3W36COnE4&t=2813s) The Vision for `std::regex` and Live Demo on Compiler Explorer
Hana Dusíková reflects on the ultimate goal of compile-time regular expressions, expressing hope for future C++ revisions. She suggests that once all required language machinery is fully `constexpr`, developers should not need a specialized third-party library; standard regular expressions should handle compile-time evaluation natively. As Dusíková puts it: “So, this is just experiment and it works pretty great, but in future, we should use `std::regex` and it should be great if someone care.” The audience laughs at her dry aside about whether anyone will actually take the initiative to fix and modernize `std::regex`.
To show how accessible the library is, Dusíková opens Compiler Explorer (Godbolt) in her browser, enlarges the view, and shows the audience how to select CTRE version 2 from the library menu. She types out a live example matching the literal `"abc"` using `ctre::match` against a `std::string_view`. When compiling unoptimized code, the generated output is massive, but once compiler optimizations are turned on, the entire matcher collapses into a tiny footprint. She points out that with optimizations enabled, it compiles down to about 24 lines of assembly, eliciting laughter from the audience.
[48:56](https://www.youtube.com/watch?v=QM3W36COnE4&t=2936s) Expression Size Limits and Comparisons to Boost
The Q&A session opens with an attendee asking about limits on regular expression size. Dusíková clarifies that currently the practical ceiling is roughly 1,000 characters, constrained primarily by compiler recursion depth limits. When asked if she has formal benchmarks measuring this boundary, she explains that while she ran benchmarks, she omitted them from the presentation to avoid slide overload, but she plans to include them in the documentation or share them afterward.
Another attendee asks whether CTRE has been benchmarked against Boost.Regex or Boost.Xpressive, and how its parser generator compares to Boost.Spirit X3. Dusíková responds candidly about her lack of extensive background with Boost, drawing laughs when she admits: “Unfortunately, I'm procrastinator so I finished my slide yesterday.” She promises to run comparative benchmarks against Boost libraries and publish the results.
[50:05](https://www.youtube.com/watch?v=QM3W36COnE4&t=3005s) Functional Style, Grammars, and Compile-Time Matching
An audience member asks about the fundamental limits of translating functional-style techniques into compile-time C++, wondering whether hard technical boundaries prevent broader applications. Dusíková explains that the primary obstacle is input length. Because compile-time evaluation must operate without standard heap allocations within recursive structures, parsing massive inputs like large JSON files is impractical. Furthermore, the parser design relies on an LL(1) grammar, requiring grammars free of left recursion, though an LR or LALR approach could theoretically be built under different trade-offs.
```mermaid flowchart TD accTitle: Compile-Time vs Runtime Execution Path in CTRE accDescr: Flow diagram showing how input regex strings are processed through constexpr evaluation into compact native machine instructions.
subgraph CompileTime["Compile-Time Pipeline"] A["Regex String Literal (e.g. 'abc')"] --> B["LL(1) Parser Engine (constexpr)"] B --> C["Finite Automaton AST"] C --> D["Inline constexpr Matcher Functions"] end
subgraph Evaluation["Matching Evaluation"] D -->|"String literal input"| E["Pure Compile-Time Match (constexpr bool)"] D -->|"Runtime std::string_view"| F["Assembly Matcher (~24 lines optimized)"] end ```
Another attendee asks for clarification regarding how CTRE executes matching at compile time versus runtime, noting they were confused by how the matching engine could handle both without separate implementations. Dusíková clarifies that every matching function is marked `constexpr` and avoids dynamic memory allocations entirely. Because no stateful heap allocations occur, the exact same functional traversal can execute purely during compilation when passed compile-time constants, or compile down into tight machine code when passed a runtime `std::string_view`. She mentions that on some slides the `constexpr` keyword was omitted purely for visual brevity, and reassures the audience that her full slides would be uploaded online within an hour.
[52:00](https://www.youtube.com/watch?v=QM3W36COnE4&t=3120s) Trampolining, Translation Units, and Future Grammars
Odin Holmes steps up to praise Dusíková's work, declaring: “First of all, this is the awesomest thing that anyone's done with meta programming ever.” He then asks whether compiler recursion depth limits could be bypassed using a trampolining technique. Dusíková admits that she is unfamiliar with trampolining, prompting Holmes to offer to discuss the idea with her offline.
The discussion then covers practical build-time management and grammar complexity: - **Extern templates and translation units:** An attendee asks if `extern template` can prevent slow re-compilations across multiple translation units. Dusíková explains that developers can wrap the regular expression match inside a normal wrapper function defined in a single translation unit, ensuring the pattern compiles only once and keeps overall project build times low. - **Parsing more complex languages:** Another attendee asks whether grammar complexity itself poses a barrier, or if an ambitious developer could write an `eval()` or a full C++ parser using these techniques. Dusíková notes that grammar complexity does not hurt performance because the grammar is deterministic; the only true constraint remains the length of the input. She quips: “If you are crazy enough, yeah.”
[53:48](https://www.youtube.com/watch?v=QM3W36COnE4&t=3228s) Comparison to PCRE JIT and Parser Generators
The session concludes with questions comparing CTRE to established industry tools. An attendee asks why PCRE (Perl Compatible Regular Expressions) demonstrates such fast execution. Dusíková explains that PCRE is an exceptionally mature codebase, and modern versions integrate a Just-In-Time (JIT) compiler that translates regexes directly into native machine instructions at runtime, achieving very high speeds.
A final question notes that CTRE's compile-time automaton generation strongly resembles the approach taken by classic tools like Flex or Lex, and asks if Dusíková has benchmarked her engine against Lex-generated state machines. Dusíková brings the room to laughter with her honest reaction, stating: “I didn't benchmark it against Flex or Bison because I am scared of them.” The attendee accepts the answer with an amused "Fair enough," and the talk concludes to enthusiastic audience applause.
## Notable quotes > [43:03](https://www.youtube.com/watch?v=QM3W36COnE4&t=2583s) You should forget about variables if you do compile-time programming because it just gives you more complexity. — Hana Dusíková
> [00:00](https://www.youtube.com/watch?v=QM3W36COnE4&t=0s) So, if you have application installed on million of mobile phones, each mobile phone, every time, do the same calculation. Why? It should be constexpr. It should be pre-calculated. — Hana Dusíková
> [00:00](https://www.youtube.com/watch?v=QM3W36COnE4&t=0s) No one will use interpret of Python in their code to match string, but we are doing it with regular expressions. — Hana Dusíková
## Mentioned Hana Dusíková, Avast, WG21, Bryce Adelstein Lelbach, Marshall Clow, Jeff Snyder, Richard Smith, Odin Holmes, CTRE, std::regex, PCRE, PCRE2, GCC, Clang, MSVC, Compiler Explorer, Boost.Regex, Boost.Xpressive, Boost.Spirit, Flex, Bison, egrep
## Video description http://CppCon.org — Presentation Slides, PDFs, Source Code and other presenter materials are available at: https://github.com/CppCon/CppCon2018 — I will present a library that utilizes a feature of C++20 to build regular expressions from compile-time strings. This is a novel approach that doesn't have ordinary disadvantages of other regular expression implementations like having to use a linked library or a run-time cost of parsing and interpreting an internal finite machine.
You will see implementation details of the library and problems I have run into during its writing. In the last part of the talk, I will compare other implementations of regular expression engines and show compiled code in Compiler Explorer. — Hana Dusíková, Avast Senior Researcher
Hana is working as a senior researcher in Avast Software. Her responsibility is exploring new ideas and optimizing existing ones. She also propagates modern C++ techniques and libraries in internal techtalks and gives talks at local C++ meetups.
She studied computer science at Mendel university and subsequently taught several courses there, including: Data Structures, Computability and Complexity, and Formal Languages and Automata. — Videos Filmed & Edited by Bash Films: http://www.BashFilms.com *-----* *--* *-----*
[Source](https://www.youtube.com/watch?v=QM3W36COnE4)