How to Specialize `fmt::formatter<T>` for Custom Formatting in {fmt}

To format user-defined types with the {fmt} library, you must provide a full template specialization of fmt::formatter<T> that implements the parse and format member functions.

The {fmt} library (available at fmtlib/fmt) formats values by searching for a specialization of the class template declared in include/fmt/core.h. The primary template is intentionally deleted (see line 2758), forcing users to explicitly define how their types should be rendered. This guide demonstrates the exact interface requirements and three common implementation patterns found in the source code.

The formatter<T> Interface

A valid specialization must live in the fmt namespace and implement two specific member functions. The library invokes these automatically during format string processing.

Function Signature Purpose
parse constexpr auto parse(fmt::format_parse_context& ctx) -> fmt::format_parse_context::iterator Parses the format specification (the content between : and }) and stores options like width or precision.
format auto format(const T& value, fmt::format_context& ctx) const -> fmt::format_context::iterator Writes the formatted representation of value to the output iterator ctx.out().

In include/fmt/core.h (lines 2756–2762), the library provides a generic "native" formatter for built-in types via detail::native_formatter. For your custom type, you replace this with your own specialization.

Method 1: Inheriting from Existing Formatters

When your type maps cleanly to a built-in representation—such as converting an enum to a string—inherit from an existing formatter to reuse its parsing logic.

According to doc/api.md (lines 34–56), you can inherit fmt::formatter<std::string_view> and override only the format method. The base class handles standard specifiers like width and alignment automatically.

#include <fmt/core.h>

enum class color { red, green, blue };

template <> struct fmt::formatter<color> : fmt::formatter<std::string_view> {
  auto format(color c, fmt::format_context& ctx) const 
      -> fmt::format_context::iterator {
    constexpr const char* names[] = {"red", "green", "blue"};
    return fmt::formatter<std::string_view>::format(
        names[static_cast<int>(c)], ctx);
  }
};

Usage:

#include <fmt/format.h>

int main() {
  // Uses inherited parse() to handle {:>10} alignment and width
  fmt::print("Color: {:>10}\n", color::green);
}
// Output: "Color:      green"

This approach avoids reimplementing standard format specifier parsing while giving you control over the final string conversion.

Method 2: Implementing Custom Parsing Logic

For complex types requiring unique syntax—like a geometric point with coordinate formatting—you must implement parse manually to consume custom flags.

The following example from the source analysis demonstrates parsing a dynamic or static width specifier:

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

struct point { double x, y; };

template <> struct fmt::formatter<point> {
  int width = 0;  // 0 means no width, -1 means dynamic width '*'

  constexpr auto parse(fmt::format_parse_context& ctx)
      -> fmt::format_parse_context::iterator {
    auto it = ctx.begin();
    
    if (it != ctx.end() && *it == '*') {
      ++it;
      width = -1;  // Signal dynamic width
    } else {
      const char* start = it;
      while (it != ctx.end() && std::isdigit(*it)) ++it;
      if (it != start) 
        width = std::stoi(std::string(start, it));
    }
    
    if (it != ctx.end() && *it != '}')
      throw fmt::format_error("invalid format for point");
    return it;
  }

  auto format(const point& p, fmt::format_context& ctx) const
      -> fmt::format_context::iterator {
    auto out = ctx.out();
    std::string spec = (width > 0) ? fmt::format(">{}", width) : "";
    
    out = fmt::format_to(out, "({:" + spec + "}", p.x);
    out = fmt::format_to(out, ", {:" + spec + "}", p.y);
    return fmt::format_to(out, ")");
  }
};

Key requirements for parse:

  • It must return an iterator pointing to the closing }.
  • It should be marked constexpr when possible.
  • Errors should be reported via throwing fmt::format_error rather than other exceptions, or by calling fmt::report_error in constant evaluation contexts.

Method 3: Conditionally Enabling Formatters with SFINAE

You can create generic formatters for families of types using SFINAE. As shown in include/fmt/core.h (lines 2756–2762), the library uses std::enable_if_t to enable certain formatters only when T satisfies specific type traits.

#include <type_traits>

template <typename T>
struct fmt::formatter<T, char, std::enable_if_t<std::is_enum_v<T>>> {
  // Implementation for all enums...
};

This pattern allows you to define formatting behavior for template metaprogramming patterns without explicitly listing every type.

Critical Constraints and Best Practices

When specializing fmt::formatter<T>, adhere to these rules derived from the source code:

  • Namespace placement: The specialization must reside in the global fmt namespace, not a nested namespace or your own.
  • Conflict prohibition: You cannot provide both a formatter<T> specialization and a format_as overload for the same type (see doc/api.md, lines 65–67). Choose one mechanism.
  • Output destination: The format function must write to ctx.out(). Use fmt::format_to or the detail::write utilities to emit content.
  • Const correctness: The format method should be marked const because the formatter object may be reused across multiple formatting calls.

Summary

  • Specialize fmt::formatter<T> in the fmt namespace to enable formatting for custom types.
  • Implement parse to consume format specifiers (width, alignment, etc.) and format to write the output.
  • Inherit from existing formatters like fmt::formatter<std::string_view> to reuse standard parsing logic for simple conversions.
  • Implement manually when you need bespoke syntax, ensuring parse returns an iterator to } and format writes to ctx.out().
  • Do not combine formatter<T> specializations with format_as functions for the same type.

Frequently Asked Questions

Can I partially specialize fmt::formatter for a template class?

No, the {fmt} library requires full specializations. For template classes, you must fully specialize for each concrete instantiation or use SFINAE in the third template parameter (the Enable parameter) to conditionally match a family of types, as demonstrated in include/fmt/core.h line 2756.

Why does my compiler say the formatter base class is deleted?

The primary template fmt::formatter<T, Char, Enable> is defined with a deleted constructor at line 2758 of core.h. This error indicates you haven't provided a full specialization for your specific type T, or the specialization is not visible in the fmt namespace where the library looks it up.

Should parse be constexpr and format be const?

Yes. Mark parse as constexpr to support compile-time format string validation. Mark format as const because the formatter instance may be reused or stored by the library. The signatures should match: constexpr auto parse(format_parse_context&) and auto format(const T&, format_context&) const.

How do I report errors from the parse function?

Use throw fmt::format_error("message") for runtime errors, which the library catches and converts to appropriate exceptions. In C++20 constexpr contexts, you should call fmt::report_error("message") to trigger a compile-time error for invalid format strings.

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 →