How to Implement Custom Formatters for Your Own Types in fmtlib
You can implement custom formatters in fmtlib by specializing the fmt::formatter<T> template for your type and defining parse() and format() methods, or by providing a format_as() overload for simple type conversions.
The {fmt} library formats user-defined types by dispatching to specializations of the formatter template. When you implement custom formatters for your own types in fmtlib, you replace the primary template's deleted constructor with a concrete implementation that knows how to serialize your data structure according to the library's compile-time interface.
Understanding the Formatter Interface
All formatting dispatch logic begins in include/fmt/core.h. The library instantiates the primary template:
template <typename T, typename Char = char, typename Enable = void>
struct formatter {
formatter() = delete; // forces compilation error until specialized
};
Because the primary constructor is deleted, any attempt to format a type without a valid specialization results in a compile-time error. To make your type formattable, you must provide a specialization that implements at least the format() method, and optionally parse() if you need to handle custom format specifiers.
Method 1: Reusing an Existing Formatter
For types that map cleanly to string representations, inherit from fmt::formatter<std::string_view> or another existing formatter. This approach delegates specifier handling to the base class while you only implement the conversion logic.
#include <fmt/core.h>
struct Point { int x, y; };
template <> struct fmt::formatter<Point> : fmt::formatter<std::string_view> {
auto format(const Point& p, fmt::format_context& ctx) const {
std::string s = std::to_string(p.x) + "," + std::to_string(p.y);
return fmt::formatter<std::string_view>::format(s, ctx);
}
};
// Usage
fmt::print("The point is {}\n", Point{3, 7}); // Output: The point is 3,7
This specialization lives in the fmt namespace and inherits the base format() method, allowing it to handle width and alignment specifiers automatically.
Method 2: Creating a Full Custom Formatter
When you need custom parsing logic for format specifiers (such as precision control), implement both parse() and format() methods. The parse() method receives a format_parse_context to read the format string, while format() receives the value and an output iterator.
#include <fmt/core.h>
#include <sstream>
struct Vec3 { double x, y, z; };
template <> struct fmt::formatter<Vec3> {
constexpr auto parse(fmt::format_parse_context& ctx) {
auto it = ctx.begin(), end = ctx.end();
if (it != end && *it == '.') {
++it;
precision_ = 0;
while (it != end && std::isdigit(*it)) {
precision_ = precision_ * 10 + (*it - '0');
++it;
}
}
return it;
}
template <typename FormatContext>
auto format(const Vec3& v, FormatContext& ctx) const {
std::ostringstream oss;
oss.setf(std::ios::fixed);
oss.precision(precision_);
oss << '(' << v.x << ", " << v.y << ", " << v.z << ')';
return fmt::format_to(ctx.out(), "{}", oss.str());
}
private:
int precision_ = 6;
};
// Usage with custom precision
fmt::print("Vec = {: .2}\n", Vec3{1.2345, 6.789, 0.12}); // Vec = (1.23, 6.79, 0.12)
The parse() method must return an iterator pointing past the format specifiers it consumed, allowing the library to verify that the format string is valid at compile time.
Method 3: Using format_as for Simple Conversions
For cases requiring only a simple type conversion without complex specifier support, provide a format_as() overload in the same namespace as your type. This avoids the boilerplate of a full template specialization according to the doc/api.md guidelines.
#include <fmt/core.h>
struct Money { long cents; };
inline fmt::string_view format_as(const Money& m) {
static thread_local std::string buf;
buf = std::to_string(m.cents / 100) + '.' + std::to_string(m.cents % 100);
return fmt::string_view(buf);
}
// Usage without explicit formatter specialization
fmt::print("Price: ${}\n", Money{1234}); // Output: Price: $12.34
The library detects this overload through argument-dependent lookup and formats the result using the standard formatter for fmt::string_view.
Key Implementation Files
The following files define the mechanisms described above:
include/fmt/core.h— Contains the primaryformattertemplate with its deleted constructor and the dispatch logic that instantiates user specializations.doc/api.md— Documents the official interface requirements for custom formatters andformat_asoverloads, including namespace placement rules.
Summary
- Inherit from existing formatters when your type converts easily to a string or standard type, leveraging specifier handling from the base class.
- Implement
parse()andformat()directly when you need to interpret custom format specifiers like precision or alignment flags. - Use
format_as()overloads for lightweight conversions that do not require parsing format strings or maintaining state betweenparse()andformat()calls. - The primary template in
include/fmt/core.hensures type safety by deleting the generic constructor, forcing explicit specialization for every formattable user type.
Frequently Asked Questions
Do I need to implement both parse() and format() in a custom formatter?
You must implement format(), but parse() is optional. If you omit parse(), the formatter uses the default implementation that verifies the format string is empty (no custom specifiers). Implement parse() only when you need to interpret format specifiers like width or precision.
Can I reuse formatting specifiers from standard types like float or int?
Yes. By inheriting from fmt::formatter<float> or fmt::formatter<int>, your custom formatter gains support for all standard numeric format specifiers. You only need to implement the conversion from your type to the underlying type in your format() method.
What is the difference between format_as and a full formatter specialization?
format_as provides a one-way conversion to a formattable type without supporting custom specifiers for your type. A full formatter<T> specialization allows you to parse format strings and control output character-by-character. Use format_as for simple projections and full specializations for complex formatting logic.
Where does fmtlib look for my formatter specialization?
The library instantiates fmt::formatter<T> in the fmt namespace. Your specialization must be declared in the global namespace or the fmt namespace before the first call to fmt::format with your type. The primary template definition in include/fmt/core.h performs substitution failure unless it finds your valid specialization.
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 →