How to Write a Custom Formatter for User-Defined Types in {fmt}
{fmt} formats user-defined types by specializations of the class template fmt::formatter<T, Char>, which you implement with parse() to handle format specifiers and format() to write the output representation.
The {fmt} library (fmtlib/fmt) provides fast, type-safe formatting for C++, but integrating your own structs and classes requires teaching the library how to display them. According to the source code, this is accomplished by specializing the fmt::formatter template declared in include/fmt/format.h and implementing two required member functions.
Understanding the fmt::formatter Specialization
The core mechanism relies on template specialization. When you call fmt::print or fmt::format with a custom type, the compiler searches for a specialization of fmt::formatter<T, Char> where T is your type and Char defaults to char.
The primary template resides in include/fmt/format.h. To add support for your type, you provide a full specialization inside the fmt namespace containing two methods:
parse– Reads the format specifier string (e.g., width, precision, or custom flags) from aformat_parse_contextformat– Converts the value to text and writes it into aformat_contextusingfmt::format_to
Creating a Basic Custom Formatter
For types that do not require custom formatting flags, implement a minimal parse method that immediately returns and a format method that constructs the string representation.
Consider a simple Point struct:
struct Point {
int x, y;
};
/* Specialize fmt::formatter for Point */
template <>
struct fmt::formatter<Point> {
// No format specifier supported – ignore the parse step.
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
// Return an iterator pointing to the end of the specifier.
return ctx.begin();
}
// Convert Point to a string like "(x,y)"
template <typename FormatContext>
auto format(const Point& p, FormatContext& ctx) const -> decltype(ctx.out()) {
// Delegate to the library’s built-in integer formatter.
return fmt::format_to(ctx.out(), "({},{})", p.x, p.y);
}
};
Usage requires only the standard headers:
#include <fmt/format.h>
Point pt{3, 7};
fmt::print("The point is {}.\n", pt); // → The point is (3,7).
The parse Method
The parse function receives a format_parse_context iterator pointing to the character after the opening {. If you do not support custom specifiers, simply return ctx.begin() immediately. The library assumes the format specifier ends at the closing }.
The format Method
The format function receives a const reference to your object and a format_context. Use fmt::format_to(ctx.out(), format_string, args...) to write output. This function returns an iterator to the insertion point, which you must return from your format method.
Handling Format Specifiers
For advanced use cases—such as accepting precision or width—store parsed values as member variables and reference them during formatting.
The following Vec3 formatter accepts an optional precision specifier like {:.3f}:
struct Vec3 {
double x, y, z;
};
template <>
struct fmt::formatter<Vec3> {
// Store desired precision (default 2)
int precision = 2;
// Parse something like {:.3f}
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
auto it = ctx.begin();
if (it != ctx.end() && *it == '.') {
++it;
precision = 0;
while (it != ctx.end() && std::isdigit(*it)) {
precision = precision * 10 + (*it - '0');
++it;
}
}
// Reject unexpected characters.
if (it != ctx.end() && *it != '}')
throw fmt::format_error("invalid format for Vec3");
return it;
}
// Format each component with the requested precision.
template <typename FormatContext>
auto format(const Vec3& v, FormatContext& ctx) const -> decltype(ctx.out()) {
std::string fmt_str = "{:." + std::to_string(precision) + "f}";
return fmt::format_to(
ctx.out(),
"(" + fmt_str + ", " + fmt_str + ", " + fmt_str + ")",
v.x, v.y, v.z);
}
};
This enables precision control:
Vec3 v{1.2345, 6.789, 0.12345};
fmt::print("Default: {}\n", v); // → (1.23, 6.79, 0.12)
fmt::print("Precise: {:.4f}\n", v); // → (1.2345, 6.7890, 0.1235)
Parsing Custom Flags
Inside parse, iterate the context iterator to examine characters. The iterator starts at the first character of the format specifier and ends at ctx.end(). Extract numerical values manually, then validate that the final character is }.
Dynamic Format String Construction
When you need to apply parsed parameters (like precision) to sub-values, construct a temporary format string using std::to_string or similar utilities, then pass it to fmt::format_to along with the data members.
Key Implementation Files
Reference the following source files in fmtlib/fmt to study production-grade patterns:
include/fmt/format.h– Contains the declaration oftemplate <typename T, typename Char> struct formatter, which you must specialize.include/fmt/std.h– Defines formatters for standard library types likestd::chrono::durationandstd::optional. These demonstrate best practices for complex parsing and nested formatting.doc/api.md– Documents the formatter interface requirements and parsing rules.test/format.cc– Contains test cases with temporary types and their formatter specializations, providing practical usage examples.
Summary
- Specialize
template<> struct fmt::formatter<YourType>inside thefmtnamespace to teach the library your type’s layout. - Implement
parse()to consume format specifiers fromformat_parse_context; returnctx.begin()if you require no custom flags. - Implement
format()to emit the desired representation viafmt::format_to(ctx.out(), ...), optionally delegating to existing formatters for primitive members. - Study
include/fmt/std.hfor authoritative examples of handling optional values, ranges, and time points. - Include
<fmt/format.h>(or<fmt/core.h>for basic usage) before your specialization to ensure the primary template is visible.
Frequently Asked Questions
Where is the fmt::formatter template defined?
The primary template is declared in include/fmt/format.h. The compiler attempts to instantiate fmt::formatter<T, Char> for every type you format; providing a specialization visible in the fmt namespace overrides this lookup.
Do I need to implement both parse and format methods?
Yes, both are required interface members. However, if your formatter does not support custom flags, implement parse as a single line returning ctx.begin() to indicate the format specifier is empty.
Can I reuse existing formatters inside my custom format method?
Absolutely. Call fmt::format_to(ctx.out(), "{}", member) or construct dynamic format strings. The library dispatches these arguments to their respective formatter specializations automatically, allowing you to compose complex formatters from primitive ones.
How do I throw errors for invalid format specifiers?
Within parse, inspect the context iterator. If you encounter unexpected characters, throw fmt::format_error("description"). This exception propagates to the caller with a diagnostic message, matching the behavior of standard library formatters.
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 →