Understanding Type Deduction for `format_string<T...>` in `fmt::print` and `fmt::format`
In the fmt library, format_string<T...> prevents the format string literal from participating in template argument deduction while allowing the compiler to deduce argument types T... from trailing parameters, enabling compile-time format string validation.
The {fmt} library (repository fmtlib/fmt) implements a sophisticated template mechanism that separates format string validation from argument type deduction. This design ensures that malformed format strings trigger static assertions at compile time rather than runtime errors, all while maintaining an intuitive API for fmt::print and fmt::format.
The fstring Foundation in base.h
The mechanism centers on the fstring class template defined in include/fmt/base.h at lines 2696 through 2744. This class wraps a string_view representing the format literal and encapsulates the compile-time validation logic.
Within the fstring<T...> constructor (lines 2711–2722), the library invokes detail::parse_format_string with a type-aware checker. This checker validates that the number and types of placeholders in the literal match the template parameter pack T.... If the literal contains mismatched braces or incompatible specifiers, the validation triggers a static_assert, producing a clear compiler error immediately.
The format_string Alias and Non-Deduced Contexts
At line 2745 of include/fmt/base.h, the library defines the user-facing type:
template <typename... T>
using format_string = typename fstring<T...>::t;
This alias template is the critical component that controls type deduction. Because format_string<T...> is an alias-dependent type, it constitutes a non-deduced context according to C++ template deduction rules. When the compiler encounters a function parameter of this type, it cannot deduce T... from the format string argument itself. Instead, deduction must occur from subsequent parameters.
How Deduction Works at the Call Site
The function signatures in include/fmt/format.h (lines 2828–2831 for print, and lines 4569–4572 for format) exploit this non-deduced context:
template <typename... T>
void print(format_string<T...> fmt, T&&... args);
Consider the following invocation:
fmt::print("{} {}", 42, std::string{"hello"});
The compiler performs deduction by examining only the trailing arguments 42 and std::string{"hello"}, deducing T... as <int, std::string>. Because format_string<T...> is non-deduced, the string literal "{}{}" does not influence template argument deduction. The compiler merely verifies that the literal can convert to the already-deduced format_string<int, std::string> type.
Compile-Time Validation Guarantees
Once T... is deduced, the constructor of fstring<T...> validates the format string against these specific types. This two-phase process—deduction followed by validation—enables the library to catch errors like the following at compile time:
// ❌ Compile-time error: too few arguments for the format string
fmt::print("{} {}", 42);
The validation ensures that every {} placeholder has a corresponding argument of compatible type, eliminating runtime format string parsing overhead and preventing security vulnerabilities associated with malformed format strings.
Wide-Character Support with wformat_string
The same deduction pattern applies to wide-character formatting in include/fmt/xchar.h. The library defines:
using wformat_string = typename basic_format_string<wchar_t, T...>::t;
Overload sets in xchar.h mirror those in format.h, applying identical non-deduced context rules to wchar_t format strings. This ensures consistent compile-time checking regardless of character width.
Runtime Format String Escape Hatch
When compile-time checking is impossible—such as when using user input—the library provides fmt::runtime(). This wrapper constructs a format_string without triggering the compile-time validator:
// Runtime format string (no compile-time validation)
fmt::print(fmt::runtime(user_input), 42);
This API design forces developers to explicitly opt out of safety checks, maintaining the default behavior of static verification for string literals.
Summary
format_string<T...>acts as a non-deduced context alias, defined at line 2745 ofinclude/fmt/base.h.- Template argument deduction occurs exclusively from trailing function arguments (
T&&... args), not from the format string parameter. fstring<T...>performs compile-time validation during construction (lines 2711–2722 ofbase.h), comparing the literal against deduced typesT....- Function overloads in
include/fmt/format.h(lines 2828–2831 and 4569–4572) combine these components to provide type-safe formatting. - Wide-character variants in
include/fmt/xchar.husebasic_format_string<wchar_t, T...>with identical deduction semantics.
Frequently Asked Questions
Why does the format string not participate in template argument deduction?
Because format_string<T...> is an alias template dependent on fstring<T...>::t, it constitutes a non-deduced context under C++ standard rules. This intentional design forces the compiler to deduce T... only from the trailing arguments pack, ensuring the format string literal is validated against already-determined types rather than influencing their deduction.
What happens if I pass a runtime-generated string to fmt::print?
Without fmt::runtime(), passing a non-constexpr string causes a compilation error because fstring requires a compile-time constant for validation. You must explicitly wrap runtime strings with fmt::runtime() to bypass static checking, creating a format_string with deferred runtime parsing.
Where does the actual format string validation occur?
Validation happens in the constructor of fstring<T...> at lines 2711–2722 of include/fmt/base.h. This constructor calls detail::parse_format_string with a type-aware checker that verifies placeholder counts and specifiers match the deduced parameter pack T... before any formatting occurs.
How does wide-character formatting differ in its type deduction?
Wide-character formatting uses wformat_string<T...>, defined in include/fmt/xchar.h as an alias for basic_format_string<wchar_t, T...>::t. The deduction mechanism remains identical to the narrow-character version, treating the format string as a non-deduced context while extracting types from trailing arguments.
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 →