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:

  1. Argument Deduction: The compiler deduces the size of the literal string (including the null terminator) as N = 8.
  2. Implicit Construction: The string literal "example" is implicitly passed to the constructor of FixedString<8>. This constructor runs inside a constant evaluation context.
  3. Template Instantiation: The constructed FixedString instance becomes the template argument Str for operator""_parse<FixedString<8>{...}>().
  4. Compile-Time Evaluation: Inside the operator, Str is a compile-time constant expression. Its members can be read using constexpr functions.

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 compile

If 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.