Custom Formatter Extension API in fmtlib: How to Format User-Defined Types
The custom formatter extension API in fmtlib enables type formatting by specializing the fmt::formatter<T> template or by defining a format_as function, with the former providing full control over parsing and output generation.
The fmtlib/fmt library provides a type-safe formatting system for C++ that can be extended to user-defined types through a well-documented extension interface. Understanding the custom formatter extension API is essential for integrating custom data structures into fmtlib's formatting pipeline, whether you are working with simple enums or complex nested objects.
The fmt::formatter Specialization Interface
The primary mechanism for extending fmtlib with user-defined types is the fmt::formatter<T> class template specialization defined in include/fmt/format.h.
Required Member Functions
According to the source code in include/fmt/format.h and the documentation in doc/api.md (lines 79-87), a valid specialization must implement two const member functions:
parse(format_parse_context& ctx): Parses format specifiers appearing after the colon in a format field (e.g.,{:<10}) and returns an iterator pointing to the closing brace.format(const T& value, format_context& ctx): Writes the formatted representation ofvalueinto the output iterator supplied byctx.
Both functions participate in the library's compile-time validation system when marked as constexpr or consteval.
Reusing Existing Formatters via Inheritance
To avoid re-implementing common parsing logic for specifiers like fill, alignment, and width, custom formatters can inherit from existing formatters. The documentation in doc/api.md (lines 34-40) demonstrates inheriting from fmt::formatter<std::string_view> to reuse its parse implementation.
#include <fmt/format.h>
enum class color { red, green, blue };
template <> struct fmt::formatter<color> : fmt::formatter<std::string_view> {
// parse is inherited
auto format(color c, fmt::format_context& ctx) const
-> fmt::format_context::iterator {
const char* name = "unknown";
switch (c) {
case color::red: name = "red"; break;
case color::green: name = "green"; break;
case color::blue: name = "blue"; break;
}
return fmt::formatter<std::string_view>::format(name, ctx);
}
};
int main() {
fmt::print("Color: {:>10}\n", color::blue); // prints “ blue”
}
This pattern leverages the base formatter's specification parsing while customizing only the output generation in the derived format method.
Handling Nested Structures with fmt::nested_formatter
For types containing sub-objects that require individual formatting specifications, fmtlib provides fmt::nested_formatter. As implemented in include/fmt/format.h (lines 99-104) and documented in doc/api.md (lines 70-78), this utility parses outer specifications and forwards member formatting to nested instances.
#include <fmt/format.h>
struct point {
double x, y;
};
template <>
struct fmt::formatter<point> : fmt::nested_formatter<double> {
auto format(point p, fmt::format_context& ctx) const
-> fmt::format_context::iterator {
return write(ctx, "(", nested(p.x), ", ", nested(p.y), ")");
}
};
int main() {
fmt::print("[{:>20.2f}]\n", point{1, 2}); // prints “[ (1.00, 2.00)]”
}
The nested() helper applies the parsed format specifications (such as width 20 and precision .2f) to each individual member of the composite type.
Alternative: The format_as Function
The custom formatter extension API also supports a simpler mechanism for types that need only basic conversion without custom parsing logic. By defining a format_as function in the same namespace as the type, you can delegate formatting to an existing type:
#include <fmt/format.h>
struct wrapper {
int value;
};
auto format_as(const wrapper& w) { return w.value; }
int main() {
fmt::print("{}\n", wrapper{42}); // prints “42”
}
As noted in doc/api.md (lines 65-68), you cannot provide both format_as and a fmt::formatter specialization for the same type, as this creates an ambiguity in the resolution mechanism.
Compile-Time Safety and Validation
The parse member function can be declared as constexpr (or consteval in C++20), enabling the custom formatter extension API to participate in compile-time format string validation. This ensures that invalid format specifications are diagnosed at compile time rather than runtime when using FMT_STRING macros or C++20 consteval contexts.
Restrictions and Limitations
The fmtlib custom formatter extension API imposes specific constraints to maintain type safety and prevent ambiguity:
- Pointer restrictions: Formatting of raw non-void pointer types is deliberately disabled. You cannot specialize
fmt::formatter<T>for pointer types. - Exclusive mechanisms: As documented in the source, providing both
format_asand aformatterspecialization for the same type results in a compile-time ambiguity error.
Summary
- The custom formatter extension API in fmtlib centers on specializing
fmt::formatter<T>withparse()andformat()member functions. - Inheritance from existing formatters (like
fmt::formatter<std::string_view>) allows reuse of parsing logic for standard specifiers. fmt::nested_formattersimplifies formatting composite types by applying specifications to individual members.format_asprovides a lightweight alternative for simple type conversions without custom parsing.- Both approaches support compile-time validation when implemented as
constexpr.
Frequently Asked Questions
What is the difference between format_as and specializing fmt::formatter?
format_as is a non-intrusive function that converts your type to a formattable type, suitable for simple cases without custom format specifiers. Specializing fmt::formatter provides full control over parsing format strings and output generation, supporting custom alignment, width, and precision specifications.
Can I create a custom formatter for pointer types?
No. The fmtlib source code explicitly disables formatting of non-void pointer types through the custom formatter extension API. This restriction prevents accidental formatting of raw memory addresses and encourages explicit handling of pointer semantics.
How do I handle format specifiers like width and alignment in my custom formatter?
Inherit from an existing formatter (such as fmt::formatter<std::string_view>) to reuse its parse implementation, or implement parse(format_parse_context& ctx) manually to interpret specifiers. The parse method receives an iterator positioned after the colon and must advance to the closing brace, storing any parsed values in member variables for use in format().
Is compile-time format string checking available for custom formatters?
Yes. By declaring your parse method as constexpr or consteval, your custom formatter participates in fmtlib's compile-time validation. This catches invalid format specifications at compile time when using FMT_STRING or C++20 format strings.
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 →