How to Create a Custom fmtlib Formatter: Required Methods Explained
To implement a custom fmtlib formatter, you must specialize fmt::formatter<T> and implement two member functions: parse() to handle format specifiers and format() to write the representation.
The fmt library (fmtlib/fmt) provides a powerful extension mechanism for formatting user-defined types through template specialization. By implementing the formatter interface, you can integrate custom types directly into fmt::format strings with full support for width, alignment, and other format specifications.
Required Methods for a Custom fmtlib Formatter
When specializing fmt::formatter<T, Char> (where Char defaults to char), you must provide exactly two methods that the library invokes during formatting operations.
The parse Method
The parse method reads the format specifier that appears after the colon in a format string (e.g., {:<10}). It stores any parsed options and returns an iterator pointing to the first character after the specifier.
According to the source code in include/fmt/format.h (lines 2758–2772), the required signature is:
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin());
This method is called once per format string to extract width, precision, alignment, and custom flags.
The format Method
The format method emits the actual string representation of your type into the output iterator provided by the formatting context. It receives the value to format and the context containing the output iterator.
The signature as implemented in the fmtlib source is:
template <typename FormatContext>
auto format(const T& val, FormatContext& ctx) const -> decltype(ctx.out());
This template approach allows the method to work with different character types and output iterators.
Minimal Implementation Skeleton
A minimal valid custom fmtlib formatter requires the following structure:
template <> struct fmt::formatter<MyType> {
constexpr auto parse(fmt::format_parse_context& ctx) -> decltype(ctx.begin()) {
// Parse format specifications or skip to end
return ctx.begin();
}
template <typename FormatContext>
auto format(const MyType& val, FormatContext& ctx) const -> decltype(ctx.out()) {
// Write formatted output
return fmt::format_to(ctx.out(), "{}", val.to_string());
}
};
Both methods must be defined even if your type does not support custom format specifiers.
Inheriting from Existing Formatters
If your type should support standard format options (width, alignment, precision), inherit from an existing formatter rather than implementing parse manually. This approach leverages the base class's parsing logic while customizing only the output generation.
For example, inheriting from fmt::formatter<int> provides automatic handling of hex, octal, and padding specifiers.
Complete Working Examples
Basic Custom Formatter
The following example formats a Point struct as (x,y) without custom specifiers:
#include <fmt/core.h>
struct Point { int x, y; };
template <> struct fmt::formatter<Point> {
constexpr auto parse(fmt::format_parse_context& ctx) -> decltype(ctx.begin()) {
return ctx.begin(); // No custom specifiers to parse
}
template <typename FormatContext>
auto format(const Point& p, FormatContext& ctx) const -> decltype(ctx.out()) {
return fmt::format_to(ctx.out(), "({},{})", p.x, p.y);
}
};
int main() {
Point pt{3, 4};
fmt::print("Point = {}\n", pt); // Output: Point = (3,4)
}
Advanced Formatter with Standard Options
This example inherits from fmt::formatter<int> to support width and alignment while formatting a wrapper type:
struct HexInt { int value; };
template <> struct fmt::formatter<HexInt> : fmt::formatter<int> {
using fmt::formatter<int>::parse; // Inherit standard parsing
template <typename FormatContext>
auto format(const HexInt& h, FormatContext& ctx) const -> decltype(ctx.out()) {
return fmt::formatter<int>::format(h.value, ctx);
}
};
int main() {
HexInt h{255};
fmt::print("{:>8x}\n", h); // Output: ff (right-aligned, hex)
}
Summary
- Two required methods: Every custom fmtlib formatter must implement
parse()andformat()member functions. - Specialization point: Create a template specialization of
fmt::formatter<T, Char>for your typeT. - Source location: The primary template and requirements are defined in
include/fmt/format.hat lines 2758–2772. - Inheritance option: Derive from existing formatters like
fmt::formatter<int>orfmt::formatter<string_view>to reuse standard format specifier parsing. - Const correctness: The
formatmethod should be markedconstas it does not modify the formatter state.
Frequently Asked Questions
What are the exact method signatures required for a custom fmtlib formatter?
Your specialization must provide constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) and template <typename FormatContext> auto format(const T& val, FormatContext& ctx) const -> decltype(ctx.out()). These signatures are strictly enforced by the primary template in include/fmt/format.h.
Can I inherit from existing fmtlib formatters to avoid writing parsing logic?
Yes. If your type wraps a primitive or string-like type, inherit from the appropriate formatter (e.g., fmt::formatter<int>) and use using declarations to bring in the base parse method. This automatically enables width, alignment, and type-specific format specifiers without additional code.
Where are the formatter requirements defined in the fmtlib source code?
The primary template definition requiring the parse and format methods is located in include/fmt/format.h at lines 2758–2772. The test/format-test.cc file contains additional reference implementations of built-in formatters that demonstrate best practices.
Do I need to implement both parse and format methods even for simple types?
Yes. Both methods are mandatory because the formatting engine expects them at compile time. For types that do not support custom specifiers, implement parse to simply return ctx.begin() without consuming any characters. The format method must always be provided to define how your type converts to a string representation.
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 →