How fmtlib Handles Different Data Types for Formatting: A Complete Guide
fmtlib formats any data type by finding a fmt::formatter specialization for that type and character set, falling back through three extensibility mechanisms when no direct match exists.
The {fmt} library provides a type-agnostic formatting engine that automatically adapts to integers, floats, strings, containers, timestamps, and user-defined types. This article examines the exact mechanisms—custom specializations, format_as functions, and stream insertion operators—that enable this flexibility, with direct references to the source code in fmtlib/fmt.
The Core Formatter Selection Mechanism
At the heart of fmtlib's type handling is the formatter<T, Char> template. When you call fmt::format("{:}", value), the engine in include/fmt/format.h instantiates formatter<T, Char> for the argument's concrete type T and character type Char.
The formatting workflow proceeds in two phases:
parse(format_parse_context& ctx)— Reads format specifications (width, precision, alignment) from the format string.format(const T& value, FormatContext& ctx)— Writes the formatted representation to the output buffer.
This design decouples format string parsing from value serialization, allowing each type to define its own formatting logic while reusing the core engine.
Three Ways to Format Custom Types
fmtlib provides three distinct extension points for user-defined types, applied in priority order:
1. Custom formatter Specialization
The most explicit approach: provide a template<typename Char> struct fmt::formatter<MyType, Char> with parse and format member functions.
Reference implementations appear throughout include/fmt/std.h, such as formatter<std::complex<T>, Char> at line 785:
struct Point { double x, y; };
template<> struct fmt::formatter<Point> {
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
template<typename FormatContext>
auto format(const Point& p, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "({:.2f}, {:.2f})", p.x, p.y);
}
};
// Usage
fmt::format("{}", Point{1, 2}); // Returns "(1.00, 2.00)"
This method gives complete control over parsing and output generation.
2. format_as Function Overload
When a type can be trivially converted to an already-formattable type, define a free function format_as(const T&) that returns the proxy type. The generic formatter detects this via SFINAE in include/fmt/std.h around line 290.
struct Money { double amount; };
inline double format_as(const Money& m) { return m.amount; }
// Usage: forwards to double formatter
fmt::print("{:.2f}\n", Money{12.5}); // Prints "12.50"
This avoids boilerplate when the existing formatter's behavior suffices.
3. Stream Insertion Operator (operator<<)
When neither specialization nor format_as exists, fmtlib falls back to ostream_formatter defined in include/fmt/ostream.h at line 77. This formatter constructs a temporary std::ostringstream, streams the value, then formats the resulting string.
struct Color { int r, g, b; };
std::ostream& operator<<(std::ostream& os, const Color& c) {
return os << "#" << std::hex << c.r << c.g << c.b;
}
// Usage
fmt::print("{}\n", Color{255, 0, 255}); // Prints "#ff00ff"
Note: This fallback incurs overhead from std::ostringstream allocation and is slower than native formatter specializations.
Built-in Type Coverage
The library ships extensive formatter specializations in dedicated headers:
| Header | Types Covered |
|---|---|
include/fmt/format.h |
Integers, floating-point, strings, pointers, bool |
include/fmt/std.h |
std::optional, std::variant, std::chrono types, std::complex, file system paths |
include/fmt/ranges.h |
Containers: std::vector, std::array, std::map, std::set, C arrays |
include/fmt/ostream.h |
Fallback for any type with operator<< |
Container Formatting Example
#include <fmt/ranges.h>
std::vector<int> v{1, 2, 3};
fmt::print("Vec: {}\n", v); // Prints "Vec: [1, 2, 3]"
The range_formatter in include/fmt/ranges.h iterates elements and applies their respective formatters recursively.
Complete Code Examples
Built-in Integer Formatting
fmt::print("Number: {:04}\n", 42); // Number: 0042
Custom Type via Formatter Specialization
struct Person { std::string name; int age; };
template<> struct fmt::formatter<Person> {
constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
template<typename FormatContext>
auto format(const Person& p, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{} ({})", p.name, p.age);
}
};
fmt::print("{}\n", Person{"Alice", 30}); // Alice (30)
Chrono Type Formatting
#include <fmt/std.h>
auto now = std::chrono::system_clock::now();
fmt::print("{:%Y-%m-%d %H:%M}\n", now); // 2024-01-15 09:30
Key Source Files
Understanding these files clarifies how fmtlib handles type dispatch:
include/fmt/core.h— Core definitions,formatentry points, and compile-time feature detectioninclude/fmt/format.h— Formatting engine andformatter<T, Char>instantiation logicinclude/fmt/std.h— Standard library type specializations andformat_asdetectioninclude/fmt/ostream.h—basic_ostream_formatterfallback implementationinclude/fmt/ranges.h—range_formatterfor iterable types
Summary
- Type dispatch occurs through
formatter<T, Char>template instantiation informat.h - Extension priority: custom specialization >
format_as>operator<<fallback - Performance: Native formatters fastest,
format_asequivalent to proxy type,ostream_formatterslowest due to stringstream overhead - Header organization: Core engine separate from standard library extensions and range support
- SFINAE-based detection in
std.henables automaticformat_asrouting without macro machinery
Frequently Asked Questions
How do I format a custom struct with fmtlib?
Provide a fmt::formatter specialization with parse and format member functions. The parse method handles format specifications; format writes output via fmt::format_to(ctx.out(), ...). See include/fmt/std.h for reference implementations.
What is the difference between format_as and a custom formatter?
format_as converts your type to an existing formattable type with no format specification parsing—simpler but less flexible. A custom formatter specialization handles arbitrary format strings and provides full control over output generation.
Why does my type use operator<< instead of a custom formatter?
If no fmt::formatter specialization exists and no format_as function is visible, fmtlib automatically selects basic_ostream_formatter from include/fmt/ostream.h. This streams through std::ostringstream and formats the resulting string.
Are built-in formatters faster than operator<< fallback?
Yes. Native formatter specializations write directly to the output buffer. The ostream_formatter allocates a std::ostringstream, performs stream insertion, extracts the string, then formats it—typically 3-10× slower depending on the type.
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 →