Skip to content
C++
Library
since C++11
Advanced

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++11

Input 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:

AliasUnderlying type
std::sregex_iteratorregex_iterator<string::const_iterator>
std::cregex_iteratorregex_iterator<const char*>
std::sregex_token_iteratorregex_token_iterator<string::const_iterator>
std::cregex_token_iteratorregex_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

cpp
// 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

cpp
#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.

cpp
#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.

cpp
#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.

cpp
#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

cpp
#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:

cpp
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:

cpp
const std::regex re(R"(\bERROR\b)",
    std::regex::ECMAScript | std::regex::optimize);  // C++11

Use 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

FlagGrammarNotes
std::regex::ECMAScriptECMAScript / JavaScriptDefault. Supports (?:...), (?=...), (?!...), non-greedy *?, backreferences. Most expressive.
std::regex::basicPOSIX BREUses \( and \) for groups. { and } are literals unless escaped.
std::regex::extendedPOSIX ERECloser to egrep. Parentheses are metacharacters without backslash.
std::regex::awkawk dialectLike extended, but \n is a valid escape in bracket expressions.
std::regex::grepgrepBRE plus newlines act as alternation operators.
std::regex::egrepegrepERE plus newlines act as alternation operators.

Modifier flags

These combine freely with a grammar flag or with each other:

cpp
// 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:

cpp
// 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:

cpp
#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.

cpp
// 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&lt;name&gt;)
  • No lookahead assertions ((?=...), (?!...))
  • No lookbehind assertions ((?&lt;=...), (?&lt;!...))

RE2 does support named captures via (?P&lt;name&gt;re) or (?&lt;name&gt;re). Results are queried through RE2::FullMatch, RE2::PartialMatch, or RE2::FindAndConsume with typed output arguments:

cpp
#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 (?&lt;name&gt;...) or (?P&lt;name&gt;...).

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):

cpp
#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:

cpp
#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

NeedRecommendation
Patterns known at compile time, maximum speedCTRE
Dynamic patterns, untrusted input, linear-time guarantee requiredRE2
Full Perl-compatible features (backreferences, lookahead), maximum runtime speedPCRE2 with JIT
Already using Boost, need ICU UnicodeBoost.Regex
No external dependency, patterns compiled once, moderate complexitystd::regex

See Also

  • std::regex_match / std::regex_search β€” single-match entry points
  • std::match_results / std::smatch β€” the match container type yielded by regex_iterator
  • std::sub_match β€” the half-open iterator pair type representing each capture group
  • std::regex_error β€” exception thrown for malformed patterns, with a code() member from std::regex_constants::error_type