How fmtlib Implements Compile-Time Format String Validation in C++20
fmtlib implements compile-time format string validation through constexpr parsing utilities in include/fmt/compile.h that convert literal format strings into compile-time ASTs, using C++20 features like class non-type template parameters and if constexpr to detect syntax errors, mismatched arguments, and invalid specifiers during compilation rather than at runtime.
The fmtlib/fmt repository provides a zero-overhead formatting library that leverages C++20 capabilities to perform complete format string validation at compile time. This mechanism transforms potential runtime crashes and logic errors into immediate compilation failures with clear diagnostics, ensuring type safety without sacrificing performance.
The FMT_COMPILE Macro and compiled_string Tag
The entry point for compile-time validation is the FMT_COMPILE macro defined in include/fmt/compile.h. According to the source code at line 38, this macro expands to FMT_STRING_IMPL(s, fmt::compiled_string) when the compiler supports if constexpr and return-type deduction.
#define FMT_COMPILE(s) FMT_STRING_IMPL(s, fmt::compiled_string)
This macro wraps the literal format string s in the compiled_string tag class. When you invoke fmt::format with this tagged type, the overload resolution detects it via is_compiled_string<S>::value and routes the call to detail::compile<T...>(S{}) at line 54, bypassing the runtime parsing path entirely.
Recursive Constexpr Parsing Architecture
The core validation logic resides in detail::compile_format_string, a recursive constexpr function that transforms the string literal into a compile-time AST (Abstract Syntax Tree).
At lines 66-67 of include/fmt/compile.h, the detail::compile function creates a basic_string_view of the literal and invokes the parser:
constexpr auto result = detail::compile_format_string<
detail::type_list<Args...>, 0, 0, ...>(fmt);
The parser at lines 88-122 examines each character of the format string and constructs AST nodes:
- Escape sequences (
{{or}}) becometextorcode_unitnodes - Replacement fields (
{}) becomefieldobjects - Named fields (
{name}) becomeruntime_named_fieldobjects - Fields with format specs (
{:...}) becomespec_fieldobjects
Static Type Checking and Argument Resolution
The parser performs static type validation using type_list and template metaprogramming. As shown at lines 19-20, the library maps the N-th argument to its deduced type:
using type = remove_cvref_t<decltype(detail::get<N>(
std::declval<Args>()...))>;
For named arguments, get_arg_index_by_name searches the template argument pack at compile time, enabling checks like "argument 2 must be convertible to int". When parsing format specifiers (lines 107-109), the code instantiates formatter<T, Char> for the deduced type and validates the specifiers using the same machinery as runtime formatting:
auto f = formatter<T, Char>();
auto end = f.parse(ctx);
C++20 Language Features Enabling Validation
The implementation relies on three key C++20 features introduced in include/fmt/compile.h:
Class non-type template arguments allow the literal string itself to become a template parameter via detail::fixed_string, enabling full compile-time introspection of the string contents.
if constexpr drives the branch-heavy parsing logic, discarding unreachable code paths during compilation rather than instantiating them.
Return-type deduction (auto with constexpr) lets the parser return different AST node types (text, field, spec_field) without explicit type names, building a heterogeneous AST tree.
Error Handling via Constexpr Throw
When the parser encounters malformed syntax—such as unmatched braces, unknown argument names, or type mismatches—it triggers FMT_THROW(format_error(...)). Because this occurs within a constexpr context, the throw expression produces a compilation error rather than a runtime exception. This mechanism converts format string bugs into hard compilation failures with diagnostic messages pointing directly to the offending format string.
Building the Compile-Time AST
Helper functions like make_concat (lines 60-62) compose parsed fragments into a single compile-time expression:
constexpr auto make_concat(L lhs, R rhs) -> concat<L, R>;
The resulting object satisfies the is_compiled_format concept and provides a format method that writes directly to an output iterator. At lines 81-84, the public API simply forwards arguments to this pre-generated code path:
cf.format(std::back_inserter(s), args...);
No string parsing occurs at runtime, delivering both type safety and optimal performance.
Practical Usage Examples
Basic Compile-Time Validation
#include <fmt/compile.h>
#include <fmt/core.h>
int main() {
// Valid: compiles successfully
std::string s = fmt::format(FMT_COMPILE("Answer: {}"), 42);
// Compile-time error: unmatched brace
// std::string bad = fmt::format(FMT_COMPILE("Missing {"), 1);
// Compile-time error: type mismatch
// std::string bad2 = fmt::format(FMT_COMPILE("{:d}"), "text");
}
Named Arguments with Static Checks
#include <fmt/compile.h>
int main() {
std::string s = fmt::format(
FMT_COMPILE("{greeting}, {name}!"),
fmt::arg("greeting", "Hello"),
fmt::arg("name", "World")
);
// Compile-time error: unknown argument name
// fmt::format(FMT_COMPILE("{unknown}"), fmt::arg("known", 1));
}
C++20 Literal Operator
#include <fmt/compile.h>
using namespace fmt::literals;
int main() {
// The _cf suffix invokes FMT_COMPILE automatically
std::string s = fmt::format("{}"_cf, 99);
}
Summary
- fmtlib implements compile-time validation in
include/fmt/compile.husing theFMT_COMPILEmacro to tag string literals for static analysis. - The
detail::compile_format_stringparser recursively constructs an AST at compile time, distinguishing text, fields, and format specifiers. - Static type checking occurs through
type_listmetaprogramming andformatter<T, Char>instantiation, ensuring argument types match format specifiers. - C++20 features including class non-type template arguments,
if constexpr, andconstexprthrow enable the parser to fail compilation on malformed input. - The resulting compiled format object eliminates runtime parsing overhead while guaranteeing type safety.
Frequently Asked Questions
What happens when compile-time validation fails?
When the constexpr parser in include/fmt/compile.h detects an error—such as mismatched braces, unknown named arguments, or incompatible types—it invokes FMT_THROW(format_error(...)). Because this occurs in a constant expression context, the throw causes a compilation error with a diagnostic message describing the format string violation, preventing the program from building until the error is fixed.
Does compile-time validation work with C++17?
No, this feature requires C++20. The implementation relies on class non-type template parameters to pass the string literal as a template argument, if constexpr for compile-time branching, and guaranteed constexpr memory allocation for the AST construction. While fmtlib supports C++11 and later, compile-time validation specifically requires C++20 support in both the library and the compiler.
How does fmtlib's compile-time validation compare to std::format?
The C++20 standard library std::format performs runtime checking of format strings by default. fmtlib's FMT_COMPILE extension provides stricter guarantees by validating syntax and types during compilation, potentially eliminating runtime overhead entirely. According to the fmtlib source, the compiled path generates specialized code that writes directly to output iterators without runtime parsing.
Can I use compile-time validation with runtime-generated format strings?
No. Compile-time validation requires the format string to be a compile-time constant known when FMT_COMPILE is invoked. Runtime strings must use the standard fmt::format runtime interface, which performs validation during execution. The library distinguishes these paths through the is_compiled_string type trait, routing compile-time strings to detail::compile and runtime strings to the standard parser in include/fmt/format.h.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →