Compile-Time String Structure: What C++ Reflection Unlocks Beyond std::format
Source: isocpp
There is a useful thought experiment buried in Barry Revzin’s recent writeup on meta::substitute: what if a format string were not just a template for value substitution, but a structured program that runs at compile time? The answer requires a surprisingly small amount of new machinery, and that machinery is already inside the reflection proposal.
The Limits of std::format
C++20’s std::format was a genuine improvement over printf and std::ostringstream. It validates format strings at compile time, handles type-safe argument passing, and provides extension points for custom formatters. For the common case, it is excellent.
But the compile-time check it performs is narrow. It verifies that argument count matches placeholder count, and that each argument type satisfies the std::formattable concept. The format string itself is effectively opaque past that point. You cannot, at compile time, inspect the internal structure of the placeholders, rearrange them, inject per-site logic, or produce anything other than a flat sequence of substituted values.
This is not a flaw in the design so much as a precise statement of what std::format solves. The format string is a compile-time constant, but the information extracted from it stays inside the standard library’s implementation. Nothing about that structure is accessible to calling code.
Reflection and meta::substitute
P2996, the static reflection proposal authored by Wyatt Maisel, Barry Revzin, Daveed Vandevoorde, and others, introduces a mechanism for treating C++ program structure as data. Reflected entities are represented as values of type std::meta::info, a cheap, copyable, consteval-only handle to something in the program: a type, a function, a template, a data member, a template argument.
meta::substitute is one of the core operations in this API. Its signature is approximately:
namespace std::meta {
consteval info substitute(info templ, span<const info> args);
}
You give it a reflection of a template and a list of reflected template arguments, and it returns a reflection of the resulting specialization:
consteval {
auto templ = ^std::tuple;
std::vector<std::meta::info> args = {^int, ^float, ^std::string};
// Produces a reflection of std::tuple<int, float, std::string>
auto result = std::meta::substitute(templ, args);
}
Before reflection, building a template instantiation from a dynamically constructed argument list required recursive template machinery or std::apply-style indirection. meta::substitute makes it a direct operation inside a consteval context. That shift in ergonomics is larger than it looks.
Format Strings as Compile-Time Programs
Revzin’s application of this to string interpolation is where things become concrete. The idea, developed as part of his paper exploring compile-time string processing, is that if you can parse a format string at compile time and extract structured information about each interpolation site, you can perform transformations on that structure rather than merely substituting values into it.
The motivating example from his writeup is a highlighting algorithm. Given a format string that marks certain interpolation positions as highlighted, the output wraps those values in asterisks:
x=5 and y=*10* and z=hello!
Here y’s value is highlighted while x and z are not. With std::format this is not achievable cleanly. You would have to handle the highlighting logic at the call site, manually wrapping values before passing them in, which defeats the purpose of having a structured format string in the first place.
With compile-time parsing enabled by reflection, you can encode the highlight marker directly in the format string syntax, parse it in a consteval context, and generate per-site output logic automatically. The format string becomes executable metadata rather than a passive substitution template.
The step where meta::substitute enters is in constructing the right type for the formatter. Each distinct combination of interpolation sites and their markers corresponds to a different compile-time structure. meta::substitute lets you build that structure from a dynamically assembled reflected argument list:
// Illustrative, not literal API
consteval auto build_formatter_type(std::string_view fmt) {
std::vector<std::meta::info> site_types;
for (auto const& site : parse_interpolation_sites(fmt)) {
site_types.push_back(
site.highlighted ? ^HighlightedSite : ^PlainSite
);
}
// Instantiate FormatterFor<PlainSite, HighlightedSite, PlainSite>
// or whatever the string dictates
return std::meta::substitute(^FormatterFor, site_types);
}
The resulting type encodes the complete structure of the format string in its template arguments. The compiler generates fully specialized code for it; there is no runtime dispatch over site types.
Revzin notes in the article that he had previously assumed this approach was not possible because it seems to require the format string to be a compile-time constant to drive compile-time parsing. It turns out that constraint is naturally satisfied for string literals, which is precisely the common case. The design unlocks itself.
Why This Pattern Matters Outside Formatting
String formatting is a clear demonstration because the problem domain is universally familiar. But meta::substitute is a general-purpose tool, and the same pattern applies anywhere you want to construct a type from dynamically assembled metadata.
A compile-time SQL query validator could map column names to C++ types, building a reflected row type matched to the query structure. A serialization library could generate optimized encoders for specific struct layouts without macros. A command dispatch framework, the kind of thing I find myself writing for Discord bots, could build dispatch tables from reflected command signatures rather than requiring explicit registration.
In each case the shape is identical: extract structured information at compile time, assemble a reflected argument list, call meta::substitute, work with the resulting type.
For perspective, Python has had runtime equivalents through type(name, bases, dict) for decades, and Rust’s procedural macros handle a similar niche by operating on syntax trees during compilation. Neither offers exactly what C++ reflection provides: direct manipulation of the type system at the point of use, inside ordinary consteval functions, with full access to surrounding scope and no macro system boundary to cross. Rust’s format! validates format strings at compile time through a built-in compiler mechanism rather than a user-extensible API, which is why you cannot implement format! yourself in Rust but you will be able to implement equivalents in C++ once P2996 lands.
The Current State
P2996 has been progressing through the committee and has received experimental implementation work in both the EDG and Clang frontends. The compiler explorer reflection branch has supported experimentation for some time. meta::substitute is part of the core API surface and is present in current drafts, though the final spelling and exact behavior may shift before standardization.
C++26 is the anticipated target. The API is not finalized, but meta::substitute or a direct equivalent is central enough to the reflection design that its presence is not in doubt.
The detail Revzin includes about implementing this an hour at a time during his daughter’s nap is not incidental. It says something about how far the ergonomics of compile-time C++ programming have come. The same work attempted five years ago would have demanded days of template metaprogramming archaeology. The reflection API turns it into something you can iterate on in short focused sessions, which is when good ideas actually get explored.