std::regex
C++ regular expressions — regex_match, regex_search, regex_replace, capture groups, iterators, and when to reach for a faster library.
std::regexsince C++11std::regex encapsulates a compiled regular expression pattern and, paired with regex_match, regex_search, regex_replace, and iterator adapters, provides ECMAScript-flavored pattern matching over std::string, std::wstring, and raw character arrays.
Overview
<regex>, introduced in C++11, exposes a grammar-agnostic engine through std::basic_regex<CharT>. The default grammar is a subset of ECMAScript — specifically ES3, which predates lookbehind assertions and named captures. Alternate grammars (POSIX BRE, POSIX ERE, awk, grep, egrep) can be selected at construction time via std::regex_constants::syntax_option_type.
The library is correct but expensive: pattern compilation allocates an NFA internally and costs on the order of microseconds to milliseconds depending on pattern complexity. Matching uses NFA simulation, which can exhibit exponential worst-case behavior on pathological patterns (ReDoS).
Grammar limitations. C++ regex is based on ES3. Lookahead ((?=…), (?!…)) works. Lookbehind ((?<=…), (?<!…)) and named captures ((?<name>…)) are not supported — they remain absent from standard <regex> through C++23.
Three core functions
// Full-string match — entire input must satisfy the pattern
bool std::regex_match(str, re);
bool std::regex_match(str, match_results, re); // also fills captures
// Substring search — find first occurrence anywhere in input
bool std::regex_search(str, re);
bool std::regex_search(str, match_results, re);
// Replace occurrences — returns new std::string
std::string std::regex_replace(str, re, fmt);
std::string std::regex_replace(str, re, fmt, flags);Examples
Validating and extracting with regex_match
#include <regex>
#include <string>
#include <print> // C++23
static const std::regex date_re{
R"(^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$)"
};
std::string input = "2026-05-22";
std::smatch m;
if (std::regex_match(input, m, date_re)) {
// m[0] — whole match; m[1..N] — capture groups (std::sub_match)
std::println("year={} month={} day={}", m[1].str(), m[2].str(), m[3].str());
// sub_match members: position(), length(), str(), first, second
std::println("day starts at offset {}", m[3].position());
}regex_match requires the entire string to satisfy the pattern. Use it for validation. Use regex_search when the pattern may appear at any offset.
Searching within a string
std::string log = "[2026-05-22 14:30:01] ERROR: disk full on /dev/sda1";
static const std::regex log_re{
R"(\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\] (\w+): (.+))"
};
std::smatch m;
if (std::regex_search(log, m, log_re)) {
std::println("date={} time={} level={} msg={}",
m[1].str(), m[2].str(), m[3].str(), m[4].str());
// prefix() and suffix() yield the text before and after the match
std::println("before match: '{}'", m.prefix().str());
std::println("after match: '{}'", m.suffix().str());
}Iterating all matches with sregex_iterator
// C++11: sregex_iterator wraps repeated regex_search calls
std::string src = "port=8080 timeout=30 retries=3";
static const std::regex kv_re{R"((\w+)=(\d+))"};
for (std::sregex_iterator it{src.begin(), src.end(), kv_re}, end;
it != end; ++it) {
const std::smatch& m = *it;
std::println("{}={}", m[1].str(), m[2].str());
}
// port=8080
// timeout=30
// retries=3Splitting with regex_token_iterator
Submatch index -1 yields the tokens between matches — a split:
// C++11: regex_token_iterator with index -1 splits on the separator
std::string csv = "alpha,beta,,gamma,delta";
static const std::regex sep{","};
std::vector<std::string> fields{
std::sregex_token_iterator{csv.begin(), csv.end(), sep, -1},
std::sregex_token_iterator{}
};
// {"alpha", "beta", "", "gamma", "delta"}Pass a positive index to extract a specific capture group from each match instead.
Replacing with back-references
std::string dates = "Born: 2026-05-22, Expires: 2027-01-01";
static const std::regex iso_re{R"((\d{4})-(\d{2})-(\d{2}))"};
// $1, $2, $3 reference capture groups in the replacement format string
std::string us = std::regex_replace(dates, iso_re, "$2/$3/$1");
// "Born: 05/22/2026, Expires: 01/01/2027"
// Replace only the first match
std::string first = std::regex_replace(
dates, iso_re, "$2/$3/$1",
std::regex_constants::format_first_only
);
// "Born: 05/22/2026, Expires: 2027-01-01"Syntax reference (ECMAScript grammar, default)
// Character classes
// [abc] literal set [a-z] range [^abc] negated set
// \d [0-9] \D [^0-9]
// \w [a-zA-Z0-9_] \W [^\w]
// \s [ \t\r\n\f\v] \S [^\s]
// . any except \n
// Anchors
// ^ start of string $ end of string
// \b word boundary \B non-word boundary
// Quantifiers — append ? for lazy (e.g. a*?)
// a? 0 or 1 a* 0 or more a+ 1 or more
// a{n} exactly n a{n,m} n to m a{n,} at least n
// Groups and lookahead
// (abc) capture group (?:abc) non-capturing group
// a|b alternation
// (?=abc) positive lookahead (?!abc) negative lookahead
// (?<=…) lookbehind — NOT supported in C++ ECMAScript dialect
// (?<name>…) named captures — NOT supported in standard <regex>Flags
namespace rc = std::regex_constants;
// Case-insensitive matching
std::regex re1{R"([a-z]+)", rc::icase};
// Multiline — ^ and $ match line boundaries (ECMAScript grammar only)
std::regex re2{R"(^\d+)", rc::multiline};
// nosubs — disable capture group recording (cheaper matching for bool tests)
std::regex re3{R"(foo(bar))", rc::nosubs};
// optimize — hint: spend more time at construction for faster repeated matching
std::regex re4{R"(\d{4}-\d{2}-\d{2})", rc::optimize};
// rc::extended selects POSIX ERE grammar — it does NOT enable verbose/comment mode
// There is no whitespace-as-comment mode in standard <regex>
std::regex re5{R"([0-9]+\.[0-9]+)", rc::extended};
// Flags combine with |
std::regex re6{R"(hello\s+world)", rc::icase | rc::optimize};Best Practices
Declare patterns static const. NFA compilation is the dominant cost — typically 100 µs–1 ms depending on pattern complexity. A pattern reconstructed on every call makes regex useless for any hot path.
// BAD — NFA compiled on every invocation
void validate_bad(const std::string& s) {
if (std::regex_match(s, std::regex{R"(\d{3}-\d{4})"})) { /* ... */ }
}
// GOOD — compiled once; static const regex is safe to read from multiple threads
void validate_good(const std::string& s) {
static const std::regex re{R"(\d{3}-\d{4})"};
if (std::regex_match(s, re)) { /* ... */ }
}Use raw string literals. R"(\d+\.\d+)" vs "\\d+\\.\\d+" — the difference grows fast with complex patterns, and raw literals eliminate a common source of bugs.
Catch std::regex_error on untrusted patterns. Construction throws std::regex_error (a std::runtime_error) for invalid patterns. Never construct std::regex from user input without a try/catch.
try {
std::regex re{user_pattern};
} catch (const std::regex_error& e) {
// e.code() returns std::regex_constants::error_type
report_error(e.what());
}Prefer rc::nosubs for boolean-only matches. When you only need a yes/no answer and the pattern has no captures you use, nosubs skips sub-match bookkeeping.
Common Pitfalls
regex_match vs regex_search confusion. regex_match fails unless the pattern accounts for the entire string. Adding .* anchors to a pattern to make it work with regex_match is a sign you want regex_search.
ReDoS with nested quantifiers. Patterns like (a+)+b on a non-matching string like "aaaaaaaaac" trigger exponential NFA backtracking. Unlike RE2, std::regex provides no linear-time guarantee. Avoid nested quantifiers on variable-length groups in security-sensitive or user-controlled inputs.
Dangling iterator references. sregex_iterator and sregex_token_iterator hold iterators into the source string. The string must outlive all iterators.
// Undefined behavior — iterator references a destroyed temporary
auto it = std::sregex_iterator{
std::string{"foo 42"}.begin(), // temporary destroyed here
std::string{"foo 42"}.end(),
re
};match_results invalidated on string mutation. std::sub_match objects inside smatch hold iterators into the original string. Modifying the string after a successful match invalidates all results.
rc::extended is not verbose mode. std::regex_constants::extended selects POSIX ERE grammar — it does not enable whitespace-as-comment mode. Standard <regex> has no equivalent to Perl's /x or Python's re.VERBOSE flag.
When not to use std::regex
For fixed-string operations, avoid <regex> entirely:
s.find("needle"); // substring search
s.starts_with("prefix"); // C++20
s.ends_with(".cpp"); // C++20
std::ranges::count(s, '\n'); // character counting // C++20For performance-critical or high-throughput workloads:
- RE2 — linear-time guarantees, safe against ReDoS, similar API surface
- PCRE2 — full PCRE syntax with JIT compilation, roughly 5–10× faster than
std::regex - Hyperscan — Intel SIMD-accelerated, simultaneous multi-pattern matching on byte streams
std::regex is appropriate when patterns are not on hot paths, external dependencies are unwelcome, and ES3-dialect ECMAScript syntax is sufficient.
Regex Syntax Flavors: ECMAScript, POSIX, Awk
std::regex supports six distinct grammars selectable at construction time. The default is ECMAScript (ES3), which is the most expressive flavor available in standard C++. POSIX ERE (rc::extended) and awk (rc::awk) are portable but lack lookahead and word boundaries.
// C++11
#include <regex>
namespace rc = std::regex_constants;
// Default: ECMAScript — lookahead (?=...) and \b word boundary work here
static const std::regex ecma_re{R"(\b\w+(?=ing)\b)"}; // matches "match" in "matching"
// POSIX ERE — no lookahead, no \b, but portable across POSIX tools
static const std::regex posix_re{R"([[:alpha:]]+[[:digit:]]*)", rc::extended};
// Awk grammar — alternation and most ERE constructs, awk-compatible semantics
static const std::regex awk_re{R"(^[0-9]+\.[0-9]+$)", rc::awk};
// Case-insensitive flag — works with all grammars
static const std::regex icase_re{R"(hello)", rc::icase};
// optimize — instructs the engine to spend more at compile time for faster repeated use
static const std::regex hot_re{R"(\d{4}-\d{2}-\d{2})", rc::ECMAScript | rc::optimize};
// Flags combine with |
static const std::regex combined{R"([a-z]+)", rc::icase | rc::optimize};Lookahead ((?=...), (?!...)) is ECMAScript-only. \b word boundaries are ECMAScript-only — POSIX ERE has no equivalent. If you need portable toolchain compatibility at the cost of expressive power, rc::extended is the right choice; otherwise stay with the default ECMAScript grammar.
Capture Groups and Named Submatches
std::smatch provides index-based access to all capture groups from the most recent match. m[0] is always the full match; m[1], m[2], … correspond to the parenthesized groups left-to-right in the pattern.
// C++11
#include <regex>
#include <string>
#include <iostream>
static const std::regex uri_re{
R"(^(https?)://([^/:]+)(?::(\d+))?(/.*)?)$"
};
std::string url = "https://example.com:8443/api/v1";
std::smatch m;
if (std::regex_match(url, m, uri_re)) {
std::cout << "scheme: " << m[1].str() << "\n"; // "https"
std::cout << "host: " << m[2].str() << "\n"; // "example.com"
std::cout << "port: " << m[3].str() << "\n"; // "8443"
std::cout << "path: " << m[4].str() << "\n"; // "/api/v1"
// Iterate all groups (index 0 = whole match, skip it)
for (size_t i = 1; i < m.size(); ++i) {
if (m[i].matched)
std::cout << "group[" << i << "] = " << m[i].str() << "\n";
}
}Standard <regex> does NOT support named capture groups. The syntax (?P<name>...) (Python) and (?<name>...) (Perl/PCRE) are both invalid in the ES3 grammar implemented by std::regex through C++23. Document your groups with comments (// group 1: scheme) and access them by index.
std::regex_iterator for All Matches
std::sregex_iterator wraps repeated regex_search calls to produce a forward iterator over every non-overlapping match in a string. It is the correct tool when you need all occurrences rather than just the first.
// C++11
#include <regex>
#include <string>
#include <vector>
std::string src = "Error at line 42, warning at line 107, error at line 203";
static const std::regex num_re{R"(\d+)"};
// Collect all match results
std::vector<std::string> numbers;
for (std::sregex_iterator it{src.begin(), src.end(), num_re},
end;
it != end; ++it) {
numbers.push_back((*it)[0].str()); // full match of each occurrence
}
// numbers = {"42", "107", "203"}
// Count matches without storing
auto count_matches = [](const std::string& s, const std::regex& re) {
return std::distance(
std::sregex_iterator{s.begin(), s.end(), re},
std::sregex_iterator{}
);
};
// Use case: extract all tokens matching a pattern (URLs, hex literals, etc.)
static const std::regex hex_re{R"(0x[0-9A-Fa-f]+)"};
std::string code = "flags = 0xFF | 0x0A; mask = 0x3C00;";
for (std::sregex_iterator it{code.begin(), code.end(), hex_re}, end;
it != end; ++it) {
std::cout << (*it)[0].str() << "\n"; // 0xFF, 0x0A, 0x3C00
}regex_search finds only the first match and returns a bool; sregex_iterator drives the search forward from where the last match ended, yielding every occurrence. For a single existence check, prefer regex_search. For collecting or counting all matches, use sregex_iterator.
std::regex_replace
std::regex_replace returns a new std::string with all matches substituted according to a format string. Back-references in the format string ($1, $2, …) expand to the corresponding capture groups; $& expands to the entire match.
// C++11
#include <regex>
#include <string>
static const std::regex iso_re{R"((\d{4})-(\d{2})-(\d{2}))"};
std::string text = "Meeting on 2026-06-22, deadline 2026-12-31.";
// Reformat ISO dates to US style: YYYY-MM-DD -> MM/DD/YYYY
std::string us = std::regex_replace(text, iso_re, "$2/$3/$1");
// "Meeting on 06/22/2026, deadline 12/31/2026."
// $& expands to the entire match — wrap every date in angle brackets
std::string wrapped = std::regex_replace(text, iso_re, "<$&>");
// "Meeting on <2026-06-22>, deadline <2026-12-31>."
// Replace only the first occurrence
std::string first_only = std::regex_replace(
text, iso_re, "$2/$3/$1",
std::regex_constants::format_first_only
);
// "Meeting on 06/22/2026, deadline 2026-12-31."
// URL-to-hyperlink substitution
static const std::regex url_re{R"((https?://[^\s]+))"};
std::string html = std::regex_replace(
"Visit https://example.com for details.",
url_re,
R"(<a href="$1">$1</a>)"
);
// "Visit <a href="https://example.com">https://example.com</a> for details."For simple literal substitutions without back-references, std::string::replace or std::string::find + std::string::substr is simpler and several orders of magnitude faster — regex_replace pays full NFA construction and simulation costs even for patterns that contain no special characters.
Performance: std::regex vs re2 vs PCRE2
std::regex is notoriously slow: pattern compilation and matching are typically 10–100× slower than dedicated libraries. Understanding the performance landscape guides the right library choice for each workload.
| Library | Construction | Match (typical) | Catastrophic backtracking | Named groups | Lookbehind |
|---|---|---|---|---|---|
std::regex | ~100 µs–1 ms | ~1–10 µs/match | Yes (NFA backtracking) | No (C++23) | No (C++23) |
| RE2 | ~10–100 µs | ~100–500 ns/match | No (linear time) | Yes | Limited |
| PCRE2 | ~50–200 µs | ~50–200 ns/match (JIT) | Yes (without limits) | Yes | Yes |
boost::regex | ~50–500 µs | ~500 ns–5 µs/match | Yes | Yes | Yes |
RE2 guarantees linear time O(n) in the length of the input for all patterns it accepts, making it safe against ReDoS regardless of pattern complexity or input content. PCRE2 with JIT compilation achieves the best throughput on cooperative inputs but retains backtracking exposure. std::regex is appropriate only on non-hot paths where adding an external dependency is unwelcome.
// C++11: compile once to amortize the dominant cost
// Pattern compilation is O(m) in pattern length but with a large constant factor;
// match is O(n*m) in the worst case for NFA-based std::regex.
static const std::regex re{R"(\d{4}-\d{2}-\d{2})", std::regex_constants::optimize};
// For throughput-critical work, prefer re2 (compile-once identical pattern):
// static const re2::RE2 re{"\\d{4}-\\d{2}-\\d{2}"};
// RE2::FullMatch(input, re); // always linear timeThe rc::optimize flag hints to the implementation to invest additional construction time for faster subsequent matches; on libstdc++ and libc++ this currently has minimal effect, but it communicates intent and is forward-compatible with improving implementations.
Compiling Regex Once: Static and Thread-Local Patterns
Constructing std::regex inside a loop or per-call function is the single most common regex performance bug in C++ code. Each construction allocates and populates an NFA, dominating the total cost by a factor of 100× or more relative to an actual match operation.
// C++11
// BEFORE — recompiles the pattern on every call: ~500 µs wasted per invocation
bool is_valid_ip_bad(const std::string& s) {
std::regex re{R"(^(\d{1,3}\.){3}\d{1,3}$)"}; // compiled here every time
return std::regex_match(s, re);
}
// AFTER — compiled exactly once at first call; safe for concurrent reads (C++11 guarantee)
bool is_valid_ip_good(const std::string& s) {
static const std::regex re{R"(^(\d{1,3}\.){3}\d{1,3}$)"};
return std::regex_match(s, re);
}static const std::regex is initialized once and safe to read from multiple threads simultaneously — the C++11 static local initialization guarantee covers this. If you genuinely need per-thread mutable regex state (e.g. dynamically constructed patterns with per-thread lifetime), use thread_local:
// C++11: thread_local regex — each thread gets its own instance, constructed once per thread
thread_local const std::regex tl_re{R"(\w+@\w+\.\w+)"};Regex compilation is O(m) in pattern length but carries a large constant (NFA node allocation, graph construction, possible DFA subset construction). A 20-character pattern may compile in 50–200 µs; a 200-character pattern with alternation may take 1–5 ms. Matching an already-compiled pattern against a 100-byte string is typically 1–10 µs — so the compile/match ratio is easily 100:1. Making patterns static const is the highest-ROI optimization available for any <regex> usage.
See Also
std::string::find— substring search without regex overhead<charconv>— fast, locale-independent number parsingstd::ranges::search— C++20 subsequence search algorithm