Compile-Time String Parsing with C++ Literal Operators
This article explains how C++ enables compile-time parsing of string
literals using user-defined literal (UDL) operator templates. It details
the transition from historical limitations to modern C++20 class
non-type template parameters (NTTP), demonstrates how the compiler
unpacks string data into structural types, and shows how
constexpr algorithms process this data during compilation
to produce type-safe values or trigger compile-time errors.
The Challenge with Traditional String Literals
In C++11 and C++14, user-defined literals for strings were restricted to runtime-style signatures:
constexpr ReturnType operator""_custom(const char* str, std::size_t len);While such an operator can be marked constexpr, the
string's characters are passed as an array pointer and a length rather
than as template arguments. Consequently, the literal's value cannot
directly configure return types, serve as non-type template arguments,
or drive template metaprogramming without cumbersome workarounds. The
numeric template literal operator
(template <char...> operator""_x()) existed in C++11,
but the language specification explicitly prohibited applying this
character-pack syntax to string literals.
The Modern Solution: Class Non-Type Template Parameters (C++20)
C++20 resolved this limitation by permitting structural class types to be used as Non-Type Template Parameters (NTTP). A literal operator template can accept a structural wrapper type instead of raw pointers.
To receive a string literal at compile time, you define a literal class that can store the characters inside a fixed-size array during construction:
template <std::size_t N>
struct FixedString {
char data[N]{};
// Must be constexpr to construct at compile time
constexpr FixedString(const char (&str)[N]) {
for (std::size_t i = 0; i < N; ++i) {
data[i] = str[i];
}
}
constexpr std::size_t size() const { return N - 1; } // Exclude null terminator
};Because FixedString satisfies the requirements of a
structural type (all base classes and non-static data members are
public, non-mutable, and of structural type), it can be used directly as
a template parameter in a literal operator template:
template <FixedString Str>
constexpr auto operator""_parse() {
// Str is available as a compile-time constant
return parse_impl<Str>();
}How the Compiler Processes the Literal
When the compiler encounters "example"_parse:
- Argument Deduction: The compiler deduces the size
of the literal string (including the null terminator) as
N = 8. - Implicit Construction: The string literal
"example"is implicitly passed to the constructor ofFixedString<8>. This constructor runs inside a constant evaluation context. - Template Instantiation: The constructed
FixedStringinstance becomes the template argumentStrforoperator""_parse<FixedString<8>{...}>(). - Compile-Time Evaluation: Inside the operator,
Stris a compile-time constant expression. Its members can be read usingconstexprfunctions.
Parsing at Compile Time
Once the string is embedded within a compile-time constant, parsing
proceeds using standard constexpr programming techniques
rather than complex template recursion.
template <FixedString Str>
constexpr auto operator""_to_int() {
long long result = 0;
for (std::size_t i = 0; i < Str.size(); ++i) {
char c = Str.data[i];
if (c < '0' || c > '9') {
throw "Invalid digit in integer literal"; // Triggers compile-time error
}
result = result * 10 + (c - '0');
}
return result;
}
// Usage:
constexpr auto val = "12345"_to_int; // Computes 12345 at compile time
// constexpr auto invalid = "12a45"_to_int; // Fails to compileIf the parsing logic encounters invalid formatting, throwing an
unhandled exception or invoking a non-constexpr action
causes constant evaluation to fail immediately, producing a diagnostic
error before emitting any object code.
Advantages of Compile-Time Literal Parsing
- Zero Runtime Overhead: String validation, transformation (such as calculating hashes, parsing SQL queries, or validating format strings), and object construction occur entirely during compilation.
- Type Safety: The parser can return different types depending on the content of the string (e.g., dynamically returning specialized tuple types based on format strings).
- Early Diagnostics: Syntax errors in domain-specific languages embedded in literals are surfaced as native compiler diagnostics rather than runtime exceptions.