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:

  1. parse(format_parse_context& ctx) — Reads format specifications (width, precision, alignment) from the format string.
  2. 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:

Summary

  • Type dispatch occurs through formatter<T, Char> template instantiation in format.h
  • Extension priority: custom specialization > format_as > operator<< fallback
  • Performance: Native formatters fastest, format_as equivalent to proxy type, ostream_formatter slowest due to stringstream overhead
  • Header organization: Core engine separate from standard library extensions and range support
  • SFINAE-based detection in std.h enables automatic format_as routing 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →