How to Create User-Defined Formatters in fmtlib: A Complete Guide

You can create user-defined formatters in fmtlib by specializing the fmt::formatter<T, Char> template to implement custom parse and format member functions, or by providing a lightweight format_as overload that converts your type to a natively supported format.

fmtlib/fmt is a modern C++ formatting library that provides fast, type-safe string formatting similar to Python's str.format. While the library natively handles strings, numbers, and standard containers, formatting user-defined types requires extending the library's formatting machinery through template specializations or conversion functions.

Understanding the Formatter Architecture

The {fmt} library discovers how to format a type T by looking for a specialization of the class template fmt::formatter<T, Char>. If no specialization exists, the primary template attempts to forward the value to fmt::format_as(T) when that function is available.

Where the Machinery Lives

The formatting infrastructure is distributed across three primary headers in the repository:

  • include/fmt/format.h – Contains the primary definition of the formatter template and the hook points for user-defined specializations.
  • include/fmt/format-inl.h – Houses the default formatter specializations for built-in types like integers and floating-point numbers.
  • include/fmt/core.h – Defines core utilities including format_as and type traits used by the formatter machinery.

Method 1: Specializing fmt::formatter for Full Control

For complete control over parsing format specifiers and generating output, specialize the formatter template for your type. This approach requires implementing two member functions: parse to handle the format specification string, and format to write the representation.

Implementing the parse Function

The parse function receives a format_parse_context and must advance the iterator past any format specifiers your formatter recognizes, stopping at the closing brace }. The context provides access to the format string through ctx.begin() and ctx.end().

Implementing the format Function

The format function receives a constant reference to your object and a FormatContext. It must write to the output iterator returned by ctx.out() and return the iterator position after writing.

Complete Example: Formatting a Point Structure

The following example demonstrates a custom formatter for a Point struct that supports width and alignment specifiers:

#include <fmt/core.h>
#include <string>

struct Point {
  int x, y;
};

template <typename Char>
struct fmt::formatter<Point, Char> {
  constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
    // Accept default specifiers; advance to closing '}'
    auto it = ctx.begin();
    while (it != ctx.end() && *it != '}') ++it;
    return it;
  }

  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, 7};
  fmt::print("Point = {}\n", pt);        // Output: Point = (3,7)
  fmt::print("Point = {:>12}\n", pt);    // Output: Point =         (3,7)
}

Method 2: Using format_as for Simple Conversions

When you only need to convert your type to an already-supported format (such as a string or integer), implementing a full formatter specialization is unnecessary. Instead, provide a format_as overload in the fmt namespace.

According to the source code in include/fmt/format.h at line 2272, the primary formatter specialization automatically looks up and calls format_as when no user-defined specialization exists.

When to Use format_as

Use this approach when your type has a clear string or numeric representation and you do not need to support custom format specifiers like width, precision, or alignment flags.

Example: Converting a Person to String

#include <fmt/core.h>
#include <string>

struct Person {
  std::string name;
  int age;
};

inline std::string fmt::format_as(const Person& p) {
  return p.name + " (" + std::to_string(p.age) + ")";
}

int main() {
  Person alice{"Alice", 30};
  fmt::print("{}\n", alice);   // Output: Alice (30)
  fmt::print("{:>20}\n", alice); // Width works via string formatter
}

Advanced: Custom Format Specifiers

To support custom format options (such as outputting polar coordinates with a 'p' flag), store parsing state as data members in your formatter specialization:

template <typename Char>
struct fmt::formatter<Point, Char> {
  bool polar = false;

  constexpr auto parse(format_parse_context& ctx) {
    auto it = ctx.begin();
    if (it != ctx.end() && *it == 'p') {
      polar = true;
      ++it;
    }
    return it;
  }

  template <typename Ctx>
  auto format(const Point& p, Ctx& ctx) const {
    if (polar) {
      return fmt::format_to(ctx.out(), "{:.2f}∠{:.2f}", 
                           std::hypot(p.x, p.y), 
                           std::atan2(p.y, p.x));
    }
    return fmt::format_to(ctx.out(), "({},{})", p.x, p.y);
  }
};

// Usage: fmt::print("{:p}\n", point);

Summary

  • fmt::formatter<T, Char> is the template you specialize to create user-defined formatters, requiring parse and format member functions.
  • include/fmt/format.h contains the primary template definition and hook points for customization.
  • format_as provides a lightweight alternative when you only need to convert your type to a natively supported representation.
  • The format_parse_context handles parsing specifiers after the colon, while FormatContext provides the output iterator via ctx.out().
  • Custom formatters automatically inherit support for standard specifiers like width and alignment when properly implemented.

Frequently Asked Questions

What header files do I need to include to create a custom formatter?

You must include <fmt/format.h> or <fmt/core.h>. The primary formatter template and format_as hook are defined in include/fmt/format.h, while core utilities and type traits live in include/fmt/core.h. For implementation details of built-in formatters, reference include/fmt/format-inl.h.

Can I use format_as and a custom formatter specialization together?

No, the library selects one mechanism based on availability. The primary formatter specialization checks for format_as only when no user-defined formatter<T, Char> specialization exists. If you provide both, the explicit specialization takes precedence.

How do I handle width and alignment specifiers in my custom formatter?

The default width and alignment handling occurs before your format function receives control when you use fmt::format_to with the provided context. To manually handle these specifiers, inspect the ctx argument for width information or delegate to the library's internal formatting utilities defined in include/fmt/format.h.

Where is the format_as lookup performed in the fmtlib source?

The lookup occurs in include/fmt/format.h at approximately line 2272 within the primary formatter template specialization. This location defines the fallback mechanism that calls format_as when no explicit formatter specialization is found for a given 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 →