Regex Advanced: Iterators, Token Splitting, and Replacement
Deep dive into std::regex_iterator, regex_token_iterator, regex_replace, capture groups, and regex grammar flags introduced in C++11.
std::regex_iterator / std::regex_token_iteratorsince C++11Input iterators introduced in C++11 that traverse successive matches (or sub-matches) of a compiled std::basic_regex within a character sequence, enabling full iteration over all occurrences of a pattern in a string.
Overview
The C++11 <regex> facility is structured in three algorithmic layers: single-match primitives (regex_match, regex_search), iterative traversal (regex_iterator, regex_token_iterator), and in-place replacement (regex_replace). The iterator layer is what makes practical text processing viable β finding every occurrence of a pattern, extracting structured fields from log lines, or splitting on arbitrary delimiters.
Type aliases β the library provides concrete aliases parameterized on common iterator types:
| Alias | Underlying type |
|---|---|
std::sregex_iterator | regex_iterator<string::const_iterator> |
std::cregex_iterator | regex_iterator<const char*> |
std::sregex_token_iterator | regex_token_iterator<string::const_iterator> |
std::cregex_token_iterator | regex_token_iterator<const char*> |
Grammar flags β std::regex_constants::syntax_option_type selects which grammar the engine parses. Six grammars are available since C++11: ECMAScript (default), basic (POSIX BRE), extended (POSIX ERE), awk, grep, and egrep. The ECMAScript grammar is the most expressive β it supports lookahead, non-greedy quantifiers, non-capturing groups ((?:...)), and backreferences. The POSIX grammars exist for interoperability with POSIX utilities and should rarely be chosen in new code.
The key behavioral difference from the single-match functions: regex_iterator advances through the input sequence matching each non-overlapping occurrence, while regex_token_iterator can yield a configurable set of capture-group indices per match, or yield the gaps between matches when passed -1.
Syntax
// regex_iterator β each dereference yields const match_results<BidirIt>&
std::regex_iterator<BidirIt> it(first, last, re);
std::regex_iterator<BidirIt> end; // default-constructed = end sentinel
// regex_token_iterator β each dereference yields const sub_match<BidirIt>&
// submatch: int, initializer_list<int>, or vector<int>
// submatch = -1 β yields gaps between matches (splitting)
std::regex_token_iterator<BidirIt> it(first, last, re, submatch);
std::regex_token_iterator<BidirIt> end;
// regex_replace β string overload
std::string result = std::regex_replace(input, re, fmt);
std::string result = std::regex_replace(input, re, fmt, flags);
// regex_replace β output-iterator overload (avoids intermediate allocation)
std::regex_replace(out_it, first, last, re, fmt);Examples
1. Word-frequency map with sregex_iterator
#include <regex>
#include <string>
#include <unordered_map>
#include <iostream>
int main() {
const std::string text =
"the quick brown fox jumps over the lazy dog the fox";
const std::regex word_re(R"(\b\w+\b)"); // C++11 raw string literal
std::unordered_map<std::string, int> freq;
for (std::sregex_iterator it(text.cbegin(), text.cend(), word_re), end;
it != end; ++it)
{
++freq[(*it)[0].str()]; // sub_match[0] is always the full match
}
for (auto& [word, count] : freq) // C++17 structured bindings
std::cout << word << ": " << count << '\n';
}2. Extracting structured fields with capture-group indices
regex_token_iterator accepts an initializer list of group indices. For each match it cycles through them in order, so you get groups 1, 2, 3 for match 1, then 1, 2, 3 for match 2, and so on.
#include <regex>
#include <string>
#include <iostream>
int main() {
const std::string log =
"2024-01-15 ERROR kernel: segfault at 0x0\n"
"2024-01-16 WARN disk: slow I/O\n"
"2024-01-16 INFO net: link up\n";
// Groups: 1=date, 2=level, 3=message
const std::regex entry_re(
R"((\d{4}-\d{2}-\d{2})\s+(ERROR|WARN|INFO)\s+(.+))");
std::sregex_token_iterator it(
log.cbegin(), log.cend(), entry_re, {1, 2, 3});
std::sregex_token_iterator end;
while (it != end) {
std::string date = (it++)->str();
std::string level = (it++)->str();
std::string msg = (it++)->str();
std::cout << '[' << level << "] " << date << " β " << msg << '\n';
}
}Each logical record requires exactly three iterator advances β one per group index in the list. Failing to advance in lockstep with the group count is the most common bug when using this overload.
3. Splitting on a delimiter with submatch = -1
Passing -1 as the sub-match index yields the regions between matches, turning regex_token_iterator into a flexible string splitter that handles multi-character and regex-based delimiters.
#include <regex>
#include <string>
#include <vector>
#include <iostream>
std::vector<std::string> split(const std::string& s, const std::string& sep) {
const std::regex sep_re(sep);
std::sregex_token_iterator it(s.cbegin(), s.cend(), sep_re, -1);
return {it, std::sregex_token_iterator{}};
}
int main() {
for (auto& tok : split("one::two:::three::four", R"(:{2,})"))
std::cout << tok << '\n'; // one / two / three / four
}4. regex_replace with back-references in format strings
Format strings use $1, $2, β¦ for numbered capture groups, $& for the entire match, and $' / $` for the suffix and prefix of the match.
#include <regex>
#include <string>
#include <iostream>
int main() {
// Reformat ISO dates YYYY-MM-DD β DD/MM/YYYY
const std::string input = "Filed: 2024-01-15, expires 2024-12-31.";
const std::regex date_re(R"((\d{4})-(\d{2})-(\d{2}))");
std::cout << std::regex_replace(input, date_re, "$3/$2/$1") << '\n';
// Filed: 15/01/2024, expires 31/12/2024.
// Replace only the first match using format_first_only flag (C++11)
using F = std::regex_constants::match_flag_type;
std::cout << std::regex_replace(input, date_re, "$3/$2/$1",
F::format_first_only) << '\n';
// Filed: 15/01/2024, expires 2024-12-31.
}5. Streaming output with the output-iterator overload
#include <regex>
#include <string>
#include <iterator>
#include <iostream>
int main() {
const std::string src = "user=alice&pass=s3cr3t&token=abc123";
const std::regex secret_re(R"((pass|token)=\S+?)(?=&|$))");
// Write directly to stdout β no intermediate string allocation
std::regex_replace(
std::ostreambuf_iterator<char>(std::cout),
src.cbegin(), src.cend(),
secret_re, "$1=[REDACTED]");
// user=alice&pass=[REDACTED]&token=[REDACTED]
}Best Practices
Compile the regex exactly once. std::basic_regex construction parses and compiles the pattern β this is where nearly all the cost lives. Constructing a regex inside a hot loop or a frequently called function is the most common <regex> performance mistake. Use static const locals (thread-safe since C++11) or class members:
bool is_ipv4(const std::string& s) {
static const std::regex re(R"(^(\d{1,3}\.){3}\d{1,3}$)"); // compiled once
return std::regex_match(s, re);
}Use std::regex::optimize when the same compiled pattern will be matched against many inputs. It permits the engine to spend more effort at compile time in exchange for faster match execution:
const std::regex re(R"(\bERROR\b)",
std::regex::ECMAScript | std::regex::optimize); // C++11Use raw string literals. R"(...)" eliminates double-escaping. The pattern \\b\\w+\\.\\w+ becomes the far more readable \b\w+\.\w+.
Keep the regex alive for the lifetime of all iterators derived from it. regex_iterator and regex_token_iterator store a pointer to the std::basic_regex, not a copy. Destroying the regex while iterators are still live is undefined behavior.
Common Pitfalls
Catastrophic backtracking. Patterns such as (a+)+$ exhibit exponential time on near-matching strings. Any pattern that allows multiple overlapping ways to match the same prefix is a candidate. Test patterns against adversarial inputs before deploying, and be especially careful with patterns derived from user input.
regex_match vs regex_search confusion. regex_match requires the entire string to match; regex_search finds a match anywhere in the sequence. Using regex_match(s, re) where re matches a sub-string will silently return false β a common, hard-to-diagnose bug.
Mistaken sub-match cycling. When regex_token_iterator is constructed with multiple group indices, each call to ++it advances to the next group index, not the next match. Code that treats a multi-group token iterator as if it advances one match per increment will misalign groups after the first record.
Locale-sensitive character classes. [[:alpha:]] and the meaning of \w depend on the locale imbued in the std::regex object. For portable ASCII-only matching, use explicit character ranges ([a-zA-Z]) or stick with the default ECMAScript grammar with the default locale.
Regex Flags in Depth
All flags belong to std::regex_constants::syntax_option_type and can be combined with |. The grammar flags are mutually exclusive β at most one may be specified; if none is given, ECMAScript is used by default.
Grammar flags
| Flag | Grammar | Notes |
|---|---|---|
std::regex::ECMAScript | ECMAScript / JavaScript | Default. Supports (?:...), (?=...), (?!...), non-greedy *?, backreferences. Most expressive. |
std::regex::basic | POSIX BRE | Uses \( and \) for groups. { and } are literals unless escaped. |
std::regex::extended | POSIX ERE | Closer to egrep. Parentheses are metacharacters without backslash. |
std::regex::awk | awk dialect | Like extended, but \n is a valid escape in bracket expressions. |
std::regex::grep | grep | BRE plus newlines act as alternation operators. |
std::regex::egrep | egrep | ERE plus newlines act as alternation operators. |
Modifier flags
These combine freely with a grammar flag or with each other:
// Case-insensitive matching (C++11)
std::regex re_i(R"(\bhello\b)", std::regex::icase);
// ^ and $ match at line breaks as well as start/end of string (C++17)
std::regex re_ml(R"(^\s*//.*$)",
std::regex::ECMAScript | std::regex::multiline);
// Suppress sub-match tracking β faster when captures are unneeded (C++11)
std::regex re_fast(R"(\berror\b)",
std::regex::ECMAScript | std::regex::nosubs);
// Hint: spend more effort at compile time for faster matching (C++11)
std::regex re_opt(R"(\b\w{3,}\b)",
std::regex::ECMAScript | std::regex::optimize);
// Collate: locale-sensitive range expressions such as [a-z] (C++11)
std::regex re_loc(R"([a-z]+)",
std::regex::ECMAScript | std::regex::collate);multiline is C++17. It does not exist in C++11/14. When multiline is active, ^ asserts at the start of any line (after \n) and $ asserts at the end of any line (before \n), in addition to the start and end of the entire input. Without it, ^ and $ only match the boundaries of the whole input string, even in ECMAScript mode.
nosubs is an important performance flag. When you only need to test for a match or find the overall match span β without accessing any capture groups β passing nosubs tells the engine not to allocate or populate sub-match bookkeeping structures. For scanning large inputs with simple patterns, this can reduce matching time and allocation overhead noticeably.
Named Capture Groups: What the Standard Does Not Give You
std::regex does not support named capture groups. There is no (?<name>...) or (?P<name>...) syntax in any of the six supported grammars (ECMAScript, basic, extended, awk, grep, egrep). The C++ standard has never added them. You must use indexed groups β $1, $2, (*it)[1], etc. β for everything.
The workaround for readability is to assign index constants by name:
// Simulate named groups with named index constants
const std::regex log_re(
R"((\d{4}-\d{2}-\d{2})\s+(ERROR|WARN|INFO)\s+(.+))");
constexpr int GROUP_DATE = 1;
constexpr int GROUP_LEVEL = 2;
constexpr int GROUP_MESSAGE = 3;
std::smatch m;
const std::string line = "2024-03-01 ERROR disk: write failed";
if (std::regex_search(line, m, log_re)) {
// Reads exactly like named-group access
std::cout << m[GROUP_DATE] << '\n';
std::cout << m[GROUP_LEVEL] << '\n';
std::cout << m[GROUP_MESSAGE] << '\n';
}If your project can take on an external dependency, both PCRE2 and RE2 offer genuine named-capture support, and CTRE brings named captures into compile-time patterns. See the alternative libraries section below.
Dynamic Replacement with regex_iterator
std::regex_replace accepts only a format string β there is no overload taking a callable or lambda. The format string supports $& (whole match), $1β$N (capture groups), $` (prefix) and $' (suffix). This covers the majority of substitution tasks.
When replacement logic is dynamic β for example, transforming each match based on a lookup table or runtime calculation β you must drive it manually with sregex_iterator:
#include <regex>
#include <string>
#include <sstream>
#include <unordered_map>
// Replace each word that appears in a substitution table
std::string replace_dynamic(
const std::string& input,
const std::unordered_map<std::string, std::string>& table)
{
static const std::regex word_re(R"(\b\w+\b)");
std::ostringstream out;
auto it = std::sregex_iterator(input.cbegin(), input.cend(), word_re);
auto end = std::sregex_iterator{};
std::string::const_iterator last_pos = input.cbegin();
for (; it != end; ++it) {
const std::smatch& m = *it;
// Append text between the previous match and this one
out.write(&*last_pos,
m.prefix().second - last_pos);
// Apply substitution or copy the original word
auto found = table.find(m[0].str());
out << (found != table.end() ? found->second : m[0].str());
last_pos = m[0].second;
}
// Append any trailing text after the last match
out << std::string(last_pos, input.cend());
return out.str();
}The pattern: copy m.prefix() (text before the match), emit the replacement, update last_pos, then append any tail after the final match. This is the canonical way to perform arbitrary per-match transformations without leaving the standard library.
The std::regex Performance Problem
Compilation is the bottleneck
Constructing a std::basic_regex is expensive. The constructor parses the pattern and builds an internal representation of the automaton β this is where virtually all the cost lives. In real-world benchmarks, std::regex construction is consistently 10β100x slower than PCRE2 for equivalent patterns, and significantly slower than RE2 as well. The reason lies in implementation choices: most standard library implementations (libstdc++, libc++, and MSVC) use a backtracking NFA engine. Building that NFA from an ECMAScript pattern is an O(mΒ²) or worse operation where m is the pattern length, and the constant factors are large.
Mitigation: compile once, match many.
// BAD β regex recompiled on every call
bool is_uuid_bad(const std::string& s) {
// Pattern parsed + compiled from scratch every invocation
return std::regex_match(s,
std::regex(R"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"));
}
// GOOD β compiled exactly once, reused across all calls
bool is_uuid(const std::string& s) {
static const std::regex re(
R"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
std::regex::ECMAScript | std::regex::nosubs);
return std::regex_match(s, re);
}The static const local is initialized exactly once and is thread-safe since C++11 (the standard guarantees static-local initialization is protected against concurrent first-initialization).
Backtracking and catastrophic complexity
Backtracking NFA engines β the kind used by libstdc++ and libc++ β can exhibit exponential matching time on carefully crafted patterns. As Russ Cox documented in his 2007 analysis of regular expression matching, a pattern such as a?βΏaβΏ matching against a string of n a characters forces the engine to explore 2βΏ alternative paths before succeeding. At n=29, Perl's backtracking engine takes over 60 seconds; a Thompson NFA simulation takes microseconds β a million-fold difference.
The standard library does not guarantee which algorithm its engine uses. In practice, all three major implementations (libstdc++, libc++, MSVC STL) use backtracking. This means:
- Patterns with nested quantifiers β
(a+)+,(\w|\w)+,(a|aa)+β can turn catastrophic on adversarial input. - Pattern complexity is not your only concern; input can be the weapon. Never match user-supplied patterns against user-supplied input without review.
- For security-sensitive contexts (URL parsing, HTTP header validation, config parsing where attackers control the input), prefer RE2, which guarantees linear time.
Alternative Regex Libraries
When std::regex is not fast enough, lacks a needed feature, or its backtracking nature poses a security risk, four mature alternatives cover the space:
RE2 β Linear-time, Google-maintained
RE2 (github.com/google/re2) implements Thompson NFA simulation and guarantees O(n) matching time in the length of the input for all patterns it accepts. This makes it immune to catastrophic backtracking.
The trade-off is expressiveness. RE2 deliberately omits any feature that requires backtracking or unbounded look-around:
- No backreferences (
\1,\k<name>) - No lookahead assertions (
(?=...),(?!...)) - No lookbehind assertions (
(?<=...),(?<!...))
RE2 does support named captures via (?P<name>re) or (?<name>re). Results are queried through RE2::FullMatch, RE2::PartialMatch, or RE2::FindAndConsume with typed output arguments:
#include <re2/re2.h>
// Compile once
static const re2::RE2 date_re(R"((?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2}))");
std::string year, month, day;
if (RE2::PartialMatch("Filed: 2024-03-15", date_re,
&year, &month, &day)) {
// year="2024", month="03", day="15"
}RE2 also provides RE2::GlobalReplace (modifies a std::string in place) and RE2::Replace (first occurrence only). The library is BSD-licensed and header-friendly; add it via CMake's FetchContent or as a system package (libre2-dev on Debian/Ubuntu).
PCRE2 β Full features, JIT acceleration
PCRE2 (pcre.org) is the reference full-featured Perl-Compatible Regular Expressions library. It supports everything RE2 omits β backreferences, lookahead, lookbehind, atomic groups, possessive quantifiers β plus named captures via (?<name>...) or (?P<name>...).
Its headline performance feature is JIT compilation: after compiling a pattern, calling pcre2_jit_compile() converts it to native machine code. The JIT path is typically 3β10x faster than the interpretive match path, and significantly faster than std::regex matching for both simple and complex patterns.
The API is C-based (PCRE2 has no first-class C++ wrapper in the distribution):
#include <pcre2.h>
// Compile
int errcode; PCRE2_SIZE erroff;
pcre2_code* re = pcre2_compile(
reinterpret_cast<PCRE2_SPTR>("(?<year>\\d{4})-(?<month>\\d{2})",
PCRE2_ZERO_TERMINATED, 0, &errcode, &erroff, nullptr));
// Optional: JIT compile for maximum match speed
pcre2_jit_compile(re, PCRE2_JIT_COMPLETE);
// Match
pcre2_match_data* md = pcre2_match_data_create_from_pattern(re, nullptr);
int rc = pcre2_match(re,
reinterpret_cast<PCRE2_SPTR>("Filed: 2024-03-15"), 17,
0, 0, md, nullptr);
// Cleanup
pcre2_match_data_free(md);
pcre2_code_free(re);Thin C++ RAII wrappers around pcre2_code* are straightforward to write, or use a header-only wrapper such as pcre2pp.
Boost.Regex β The standard's ancestor
Boost.Regex (authored by John Maddock starting in 1998) was the direct reference implementation from which std::regex was standardized for C++11. They share API design, flag names, and grammar semantics. If you are already using Boost, boost::regex and its boost::sregex_iterator / boost::sregex_token_iterator equivalents are drop-in alternatives, and Boost.Regex's ICU backend adds full Unicode category matching.
Prefer std::regex over Boost.Regex for new code unless you specifically need ICU Unicode support or need to support compilers predating C++11.
CTRE β Compile-Time Regular Expressions
CTRE (github.com/hanickadot/compile-time-regular-expressions) by Hana DusΓkovΓ‘ brings regex matching entirely into constexpr evaluation. Patterns are verified at compile time β a malformed pattern is a compile error, not a runtime exception. At runtime there is no parsing, no construction cost, and no std::regex-style NFA compilation step.
Requirements: C++17 with GCC 9+ or Clang 14+; C++20 broadens compiler support.
CTRE supports named captures accessed by name as a template argument:
#include <ctre.hpp>
#include <string_view>
#include <iostream>
void parse_date(std::string_view input) {
// Pattern verified at compile time; named groups accessed by name
if (auto m = ctre::search<R"((?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2}))">(input)) {
// get<"y">() returns a sub_match-like view
std::cout << m.get<"y">() << '-'
<< m.get<"m">() << '-'
<< m.get<"d">() << '\n';
}
}CTRE implements most of the PCRE syntax with one major restriction: the pattern must be a string literal (or a constexpr character array) known at compile time. Dynamically constructed patterns are not possible. For static, well-known patterns this is the fastest option available in standard C++ β particularly useful in parsers, lexers, and hot paths.
Choosing a library
| Need | Recommendation |
|---|---|
| Patterns known at compile time, maximum speed | CTRE |
| Dynamic patterns, untrusted input, linear-time guarantee required | RE2 |
| Full Perl-compatible features (backreferences, lookahead), maximum runtime speed | PCRE2 with JIT |
| Already using Boost, need ICU Unicode | Boost.Regex |
| No external dependency, patterns compiled once, moderate complexity | std::regex |
See Also
std::regex_match/std::regex_searchβ single-match entry pointsstd::match_results/std::smatchβ the match container type yielded byregex_iteratorstd::sub_matchβ the half-open iterator pair type representing each capture groupstd::regex_errorβ exception thrown for malformed patterns, with acode()member fromstd::regex_constants::error_type