How to Create Custom Formatters in fmtlib: A Complete Guide
You create custom formatters in fmtlib by specializing the class template fmt::formatter<T> for your type T and implementing the parse and format member functions, or by inheriting from an existing formatter such as fmt::formatter<std::string_view> to reuse standard specifier parsing.
The fmtlib/fmt repository provides a fast, extensible C++ formatting library that formats values by discovering specializations of fmt::formatter<T>. To add support for user-defined types, you provide a template specialization that tells the library how to parse format specifiers and how to write the formatted representation to a format_context. The core definition of the formatter interface resides in include/fmt/format.h, while detailed examples and customization patterns are documented in doc/api.md.
Understanding the fmtlib Formatter Architecture
According to the source code in include/fmt/format.h, fmtlib formats any value by looking for a specialization of the class template fmt::formatter<T>. A valid formatter must provide two key members:
parse– Aconstexprfunction that consumes characters from aformat_parse_contextto interpret fill, alignment, width, and custom specifiers.format– A function that receives the value and aformat_context, writes the output, and returns an iterator pointing past the last character written.
The library ships with a generic formatter base that handles common specifiers, allowing you to inherit existing functionality rather than implementing everything from scratch.
Specializing fmt::formatter for User-Defined Types
Formatting Simple Enums by Inheriting Existing Formatters
For enums and simple types that map cleanly to strings, inherit from fmt::formatter<std::string_view> to automatically support standard string specifiers like alignment and width.
// color.h
#include <fmt/base.h>
enum class color { red, green, blue };
template <> struct fmt::formatter<color> : fmt::formatter<std::string_view> {
// parse is inherited from formatter<string_view>
auto format(color c, fmt::format_context& ctx) const -> fmt::format_context::iterator {
std::string_view 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);
}
};
// main.cc
#include "color.h"
#include <fmt/format.h>
int main() {
fmt::print("{:>10}\n", color::blue); // → " blue"
}
Because the specialization inherits from formatter<std::string_view>, standard fill and alignment specifiers work automatically. The format method maps the enum to a readable string and delegates the actual writing to the base class implementation.
Formatting Composite Types with nested_formatter
For classes containing multiple fields that should share the same formatting specifiers, use fmt::nested_formatter<T> as defined in include/fmt/format.h. This base class provides a full parse implementation and a nested helper that applies the same width and precision to each sub-object.
// point.h
#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 fmt::write(ctx, "(", nested(p.x), ", ", nested(p.y), ")");
}
};
// main.cc
#include "point.h"
#include <fmt/print.h>
int main() {
fmt::print("[{:>20.2f}]\n", point{1.0, 2.0});
// → [ (1.00, 2.00)]
}
The nested_formatter<double> base supplies the logic to parse width and precision specifiers. Inside format, the nested helper applies these rules to each double member, ensuring consistent formatting across the composite object.
Enabling Formatters for Type Hierarchies with SFINAE
To create a single formatter that handles all types derived from a common base, use a partial specialization with std::enable_if_t. This pattern, demonstrated in doc/api.md, leverages SFINAE to restrict the template to subclasses of your base type.
// demo.h
#include <fmt/format.h>
#include <type_traits>
struct A {
virtual ~A() = default;
virtual std::string name() const { return "A"; }
};
struct B : A {
std::string name() const override { return "B"; }
};
template <typename T>
struct fmt::formatter<T,
std::enable_if_t<std::is_base_of_v<A, T>, char>>
: fmt::formatter<std::string> {
auto format(const A& a, fmt::format_context& ctx) const -> fmt::format_context::iterator {
return fmt::formatter<std::string>::format(a.name(), ctx);
}
};
// demo.cc
#include "demo.h"
#include <fmt/print.h>
int main() {
B b;
A& a = b;
fmt::print("{}\n", a); // → "B"
}
The enable_if_t condition ensures this specialization only participates in overload resolution for types derived from A, allowing polymorphic formatting while reusing the std::string formatter implementation.
Implementing parse and format Methods
When inheriting from fmt::formatter<std::string_view> or fmt::nested_formatter<T>, you typically reuse the base parse method. However, for custom syntax you must implement your own constexpr parse function that iterates over the format_parse_context and returns an iterator to the closing }.
The format method must accept a const reference to your type and a format_context, then return an iterator (usually via fmt::format_to or by calling another formatter's format method). You can locate the return iterator type via fmt::format_context::iterator.
Summary
- Specialize
fmt::formatter<T>in thefmtnamespace to teach the library how to format your type. - Inherit from existing formatters like
fmt::formatter<std::string_view>orfmt::nested_formatter<U>to reuse specifier parsing logic. - Implement
formatto map your type to output, delegating to base formatters or usingfmt::write/fmt::format_to. - Use SFINAE with
std::enable_if_tto create generic formatters for entire type hierarchies. - Reference source files
include/fmt/format.hfor the interface anddoc/api.mdfor official examples and best practices.
Frequently Asked Questions
How do I create a custom formatter for a simple enum in fmtlib?
Specialize fmt::formatter<YourEnum> and inherit from fmt::formatter<std::string_view>. Implement only the format method to map enum values to strings, then delegate to the base formatter's format method to handle the actual output and specifiers.
What is the difference between parse and format in fmt::formatter?
The parse method is a constexpr function that reads format specifiers (like width and alignment) from a format_parse_context during compile time or runtime setup. The format method receives the actual value and a format_context, writes the final string representation, and returns an iterator past the output.
Can I use SFINAE to create a generic formatter for a class hierarchy?
Yes. Define a partial specialization of fmt::formatter<T, std::enable_if_t<std::is_base_of_v<Base, T>, char>> to match all types derived from Base. This technique, documented in doc/api.md, lets you write one formatter that handles any subclass through polymorphic references.
Where can I find examples of custom formatters in the fmtlib source code?
The official examples reside in doc/api.md, while the core interface is defined in include/fmt/format.h. Production-ready specializations for standard library types are located in include/fmt/std.h, providing reference implementations for types like std::optional and std::filesystem::path.
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 →