Skip to content

Regular Expressions

std::regex (C++11) provides regular expression matching, searching, and replacement using the ECMAScript regex grammar by default. This section covers the three main operations (regex_match regex_search``regex_replace), capture groups with std::smatchPractical patterns like email Validation, and performance considerations including catastrophic backtracking.

std::regex (C++11) provides regular expression matching, searching, and replacement [N4950 §30.9]. It is declared in <regex> and uses the ECMAScript regex grammar by default.

The library provides three main operations:

FunctionBehavior
std::regex_match(str, regex)Returns true if the entire string matches the regex
std::regex_search(str, regex)Returns true if any part of the string matches
std::regex_replace(str, regex, fmt)Replaces all matches with a formatted string

:::caution std::regex is notoriously slow on many standard library implementations (particularly GCC”s libstdc++, which uses a backtracking NFA engine). For production use with untrusted input, Consider:

  • CTRE (Compile-Time Regular Expressions): header-only, uses CTAD and template metaprogramming to compile regex patterns at compile time.
  • Hand-written parsers: for simple patterns (e.g., email validation, URL parsing), a hand-written parser is often faster and more readable.
  • RE2: Google’s regex library with guaranteed linear-time matching. :::
#include <iostream>
#include <regex>
#include <string>
void regex_match_search_demo() {
std::string text = "The price is $42.99 and the discount is $5.00";
// regex_match: entire string must match
std::regex full_pattern(R"(\d+)");
std::cout << std::regex_match("12345", full_pattern) << "\n"; // true
std::cout << std::regex_match("abc123", full_pattern) << "\n"; // false
// regex_search: any substring can match
std::cout << std::regex_search(text, full_pattern) << "\n"; // true
}

std::smatch (match results for std::string) stores the results of a regex operation, including Capture groups [N4950 §30.9.2]:

  • smatch[0]: The entire match.
  • smatch[1]``smatch[2]…: Capture groups in order of their opening parentheses.
  • smatch.prefix(): The text before the match.
  • smatch.suffix(): The text after the match.
#include <iostream>
#include <regex>
#include <string>
void capture_groups_demo() {
std::string log_line = "[2026-03-31 14:22:01] [ERROR] Connection timeout";
std::regex log_pattern(
R"(\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\] \[(\w+)\] (.+))"
);
std::smatch match;
if (std::regex_match(log_line, match, log_pattern)) {
std::cout << "Full match: " << match[0].str() << "\n";
std::cout << "Date: " << match[1].str() << "\n";
std::cout << "Time: " << match[2].str() << "\n";
std::cout << "Level: " << match[3].str() << "\n";
std::cout << "Message: " << match[4].str() << "\n";
std::cout << "Match count: " << match.size() << "\n";
// Match count: 5 (0 = full, 1-4 = groups)
}
}
#include <iostream>
#include <regex>
#include <string>
void regex_replace_demo() {
std::string input = "Name: John Doe, Age: 30, City: New York";
// Replace all "Key: Value" patterns with "Key=Value"
std::regex kv_pattern(R"((\w+):\s*(\w[\w\s]*?)(?=,\s*\w+:|$))");
std::string result = std::regex_replace(input, kv_pattern, "$1=$2");
std::cout << result << "\n";
// Name=John Doe, Age=30, City=New York
// Simple HTML tag removal
std::string html = "Hello <b>world</b>";
std::regex tag_pattern(R"(<[^>]+>)");
std::string plain = std::regex_replace(html, tag_pattern, "");
std::cout << plain << "\n";
// Hello world
}

:::note In the replacement string, $& refers to the entire match, $1..$9 refer to capture groups, and $$ is a literal $. These are defined in [N4950 §30.9.4]. :::

#include <iostream>
#include <regex>
#include <string>
#include <string_view>
class EmailValidator {
// This pattern matches RFC 5322's "addr-spec" production approximately.
// A fully RFC-compliant regex would be several thousand characters.
// This is a practical compromise:
// local-part: alphanumeric + . _ % + -
// domain: alphanumeric + - separated by dots, 2-63 chars per label
static constexpr std::string_view pattern_str =
R"(^[a-zA-Z0-9._%+-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,63}$)";
public:
static bool is_valid(std::string_view email) {
static const std::regex pattern(std::string(pattern_str));
return std::regex_match(email.data(),
email.data() + email.size(),
pattern);
}
};
void email_validation_demo() {
struct Test { std::string email; bool expected; };
Test tests[] = {
{"user@example.com", true},
{"first.last@sub.domain.co.uk", true},
{"user+tag@gmail.com", true},
{"user_name@my-company.org", true},
{"", false},
{"missing-at-sign.com", false},
{"@missing-local.org", false},
{"spaces in@address.com", false},
{"double..dots@bad.com", false},
{"no-tld@domain.", false},
};
int passed = 0;
for (const auto& t : tests) {
bool result = EmailValidator::is_valid(t.email);
std::cout << (result ? "VALID " : "INVALID")
<< " | expected: " << (t.expected ? "VALID " : "INVALID")
<< " | " << t.email << "\n";
if (result == t.expected) ++passed;
}
std::cout << "\nPassed: " << passed << "/" << sizeof(tests)/sizeof(tests[0]) << "\n";
}

The primary performance concern with std::regex is catastrophic backtracking. A regex like (a+)+b applied to the string "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaac" can take exponential time because The engine tries every possible partition of the a characters between the two nested quantifiers.

#include <chrono>
#include <iostream>
#include <regex>
#include <string>
void catastrophic_backtracking_demo() {
// SAFE: no nested quantifiers — linear time
std::regex safe_pattern(R"(^a+b$)");
std::string dangerous_input(30, 'a'); // "aaa...a" (no trailing 'b')
auto start = std::chrono::steady_clock::now();
bool safe_result = std::regex_search(dangerous_input, safe_pattern);
auto elapsed_safe = std::chrono::steady_clock::now() - start;
std::cout << "Safe pattern: " << safe_result
<< " (" << std::chrono::duration<double, std::milli>(elapsed_safe).count()
<< " ms)\n";
// DANGEROUS: nested quantifiers — exponential time
std::regex dangerous_pattern(R"(^(a+)+b$)");
start = std::chrono::steady_clock::now();
bool dangerous_result = std::regex_search(dangerous_input, dangerous_pattern);
auto elapsed_dangerous = std::chrono::steady_clock::now() - start;
std::cout << "Dangerous: " << dangerous_result
<< " (" << std::chrono::duration<double, std::milli>(elapsed_dangerous).count()
<< " ms)\n";
}

:::caution Avoid nested quantifiers in regex patterns: (a+)+``(a*)*``(a+)*. These can trigger Exponential backtracking on inputs that nearly match. If you must use them, set a timeout or use a Library with guaranteed linear-time matching (RE2, hyperscan). :::

std::regex supports multiple regex grammars selectable via the std::regex::flag_type bitmask [N4950 §30.9.1]:

FlagGrammarDescription
ECMAScriptECMAScriptDefault. JavaScript-like syntax. Supports backreferences.
basicPOSIX BREBasic Regular Expressions. \( for groups, no +/?.
extendedPOSIX EREExtended Regular Expressions. () for groups, +/?.
awkPOSIX awkLike ERE but escape semantics differ.
grepPOSIX grepLike BRE but newline handling differs.
egrepPOSIX egrepLike ERE but newline handling differs.

Additional flags that can be combined with the grammar:

FlagDescription
icaseCase-insensitive matching
nosubsDo not track capture groups (faster when groups are not needed)
optimizeHint to the implementation to favor faster matching over construction time
collateLocale-sensitive character ranges like [a-z]
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string text = "Hello World";
// Case-insensitive ECMAScript
std::regex ci_pattern("hello", std::regex::icase);
std::cout << std::regex_search(text, ci_pattern) << "\n"; // true
// No capture groups — faster for simple searches
std::regex simple(R"(\d+)", std::regex::nosubs);
std::cout << std::regex_search("abc 123 def", simple) << "\n"; // true
// POSIX extended — different syntax
std::regex posix_ext("[[:digit:]]+", std::regex::extended);
std::cout << std::regex_search("abc 456", posix_ext) << "\n"; // true
}

Regex Iterators: std::regex_iterator and std::regex_token_iterator

Section titled “Regex Iterators: std::regex_iterator and std::regex_token_iterator”

For finding all matches in a string (not just the first one), use std::regex_iterator [N4950 §30.9.2]:

#include <iostream>
#include <regex>
#include <string>
int main() {
std::string text = "The prices are $42.99, $15.50, and $199.95";
// Find all dollar amounts
std::regex price_pattern(R"(\$\d+\.\d{2})");
auto begin = std::sregex_iterator(text.begin(), text.end(), price_pattern);
auto end = std::sregex_iterator();
double total = 0.0;
std::cout << "Found prices:\n";
for (auto it = begin; it != end; ++it) {
std::smatch match = *it;
std::cout << " " << match.str()
<< " at position " << match.position() << "\n";
total += std::stod(match.str().substr(1));
}
std::cout << "Total: USD " << total << "\n";
// Output:
// Found prices:
// $42.99 at position 15
// $15.50 at position 23
// $199.95 at position 31
// Total: USD 258.44
}

std::regex_token_iterator extracts specific capture groups from all matches, or splits a string:

#include <iostream>
#include <regex>
#include <string>
#include <vector>
int main() {
// Extract all capture group 1 from matches
std::string log = "key1=val1; key2=val2; key3=val3";
std::regex kv_pattern(R"((\w+)=(\w+))");
std::vector<std::string> keys, values;
// Extract group 1 (keys)
auto key_begin = std::sregex_token_iterator(
log.begin(), log.end(), kv_pattern, 1);
auto key_end = std::sregex_token_iterator();
for (auto it = key_begin; it != key_end; ++it) {
keys.push_back(it->str());
}
// Extract group 2 (values)
auto val_begin = std::sregex_token_iterator(
log.begin(), log.end(), kv_pattern, 2);
auto val_end = std::sregex_token_iterator();
for (auto it = val_begin; it != val_end; ++it) {
values.push_back(it->str());
}
std::cout << "Keys: ";
for (const auto& k : keys) std::cout << k << " ";
std::cout << "\nValues: ";
for (const auto& v : values) std::cout << v << " ";
std::cout << "\n";
// String splitting using regex_token_iterator with -1 index
std::string csv = "red,green,blue,yellow";
std::regex comma(",");
auto word_begin = std::sregex_token_iterator(csv.begin(), csv.end(), comma, -1);
auto word_end = std::sregex_token_iterator();
std::cout << "Split: ";
for (auto it = word_begin; it != word_end; ++it) {
std::cout << "[" << it->str() << "] ";
}
std::cout << "\n";
// Output:
// Keys: key1 key2 key3
// Values: val1 val2 val3
// Split: [red] [green] [blue] [yellow]
}

std::regex_match and std::regex_search accept std::regex_constants::match_flag_type flags:

FlagDescription
match_defaultDefault behavior
match_not_bolDo not match ^ at the first position
match_not_eolDo not match $ at the last position
match_not_bowDo not match \b at the beginning of the string
match_not_eowDo not match \b at the end of the string
match_anyAny character matches . (including newline)
match_not_nullDo not match empty sequences
match_continuousOnly match at the beginning (like anchored match)
match_prev_availThe first position is not the start of the sequence
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string multi_line = "line1\nline2\nline3";
// Without match_any, . does not match newline
std::regex dot_all("line.line");
std::cout << std::regex_search(multi_line, dot_all) << "\n"; // 0 (false)
// With match_any, . matches newline
std::cout << std::regex_search(multi_line, dot_all, std::regex_constants::match_any) << "\n"; // 1 (true)
}

Always store a compiled std::regex as a static or const object when the same pattern is used Multiple times. The regex compilation is expensive, and recompiling on every call is a common Performance mistake:

#include <iostream>
#include <regex>
#include <string>
#include <vector>
class LogParser {
// Compiled once, reused for every call
static const std::regex log_pattern;
static const std::regex timestamp_pattern;
public:
static std::string extract_level(const std::string& line) {
std::smatch match;
if (std::regex_search(line, match, log_pattern)) {
return match[3].str();
}
return "UNKNOWN";
}
static std::string extract_timestamp(const std::string& line) {
std::smatch match;
if (std::regex_search(line, match, timestamp_pattern)) {
return match[1].str() + " " + match[2].str();
}
return "";
}
};
const std::regex LogParser::log_pattern(
R"(\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\] \[(\w+)\])");
const std::regex LogParser::timestamp_pattern(
R"(\[(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\])");
int main() {
std::vector<std::string> logs = {
"[2026-03-31 14:22:01] [ERROR] Connection timeout",
"[2026-03-31 14:22:02] [INFO] Retry attempt 1",
"[2026-03-31 14:22:05] [WARN] Slow response",
};
for (const auto& line : logs) {
std::cout << LogParser::extract_timestamp(line) << " "
<< LogParser::extract_level(line) << "\n";
}
}

std::regex is not thread-safe for concurrent search/match/replace operations on the same regex object [N4950 §30.9.1]. If multiple threads need to use the same pattern, either:

  1. Use a thread-local static regex (thread_local const std::regex pattern(...)).
  2. Use a separate regex object per thread.
  3. Protect access with a mutex (acceptable if regex operations are infrequent).
#include <iostream>
#include <regex>
#include <string>
#include <thread>
#include <vector>
void process_line(const std::string& line) {
// thread_local: each thread gets its own compiled regex
thread_local const std::regex pattern(R"(\[(\w+)\])");
std::smatch match;
if (std::regex_search(line, match, pattern)) {
std::cout << std::this_thread::get_id() << ": " << match[1].str() << "\n";
}
}
int main() {
std::vector<std::string> lines = {
"[ERROR] something",
"[INFO] something",
"[WARN] something",
"[DEBUG] something",
};
std::vector<std::thread> threads;
for (const auto& line : lines) {
threads.emplace_back(process_line, line);
}
for (auto& t : threads) t.join();
}

1. Using std::regex with libstdc++ for Untrusted Input

Section titled “1. Using std::regex with libstdc++ for Untrusted Input”

GCC’s libstdc++ implementation of std::regex uses a backtracking NFA engine that is exponentially Slow for certain patterns. For any production code that processes untrusted input, prefer a DFA-based or hybrid engine (RE2, hyperscan). MSVC’s STL and libc++ (Clang) have better performance But still lack guaranteed linear-time matching.

2. Forgetting to Anchor Patterns with regex_match

Section titled “2. Forgetting to Anchor Patterns with regex_match”

std::regex_match requires the entire string to match. If you forget to anchor your pattern With ^ and $You may get unexpected results with regex_search:

#include <iostream>
#include <regex>
#include <string>
int main() {
std::string input = "123abc";
// regex_match: entire string must match
std::cout << std::regex_match(input, std::regex(R"(\d+)")) << "\n"; // false
std::cout << std::regex_match(input, std::regex(R"(\d+\w*)")) << "\n"; // true
// regex_search: any substring can match
std::cout << std::regex_search(input, std::regex(R"(\d+)")) << "\n"; // true
}

3. Escaping Special Characters in Replacement Strings

Section titled “3. Escaping Special Characters in Replacement Strings”

In replacement strings passed to std::regex_replaceThe $ character has special meaning. To insert a literal $, use $, $. The matched text is $&, capture groups are $1 through $9:

#include <iostream>
#include <regex>
#include <string>
int main() {
std::string text = "price: 100 USD";
// $& refers to the entire match
std::string result = std::regex_replace(
text, std::regex(R"(\d+)"), "[$&]");
std::cout << result << "\n"; // price: [100] USD
// $$ for literal dollar sign
std::string result2 = std::regex_replace(
text, std::regex(R"(\d+)"), "$$0");
std::cout << result2 << "\n"; // price: $0 USD
}

std::regex_iterator skips zero-length matches at the same position to prevent infinite loops. If You need to capture zero-length matches (e.g., for splitting), use std::regex_token_iterator with Index -1.

This topic covers the essential concepts and techniques related to regular expressions, including key principles and practical applications.

Key concepts include:

  • core concepts and definitions
  • key principles and frameworks
  • practical applications
  • common techniques and methods
  • evaluation and critical analysis

A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.