How to Format Standard C++ Types Like std::optional and std::variant with fmtlib: A Complete Guide

You can format std::optional and std::variant types automatically by including <fmt/std.h>, which provides specialized formatter templates that output human-readable representations like optional(42) or variant(hello) without requiring manual to-string conversion.

The fmt library (fmtlib/fmt) provides first-class support for modern C++ standard library containers through the include/fmt/std.h header, enabling type-safe formatting of sum and product types directly via fmt::format or fmt::print. This article examines the architectural implementation of these formatters and demonstrates how to leverage them in production code.

Architecture of fmtlib's Standard Library Formatters

The fmt library extends its core formatting engine through template specializations that reside in separate header files to minimize compilation overhead. For standard library vocabulary types, the library provides conditional formatters that verify type support at compile time.

Optional Formatter Implementation

The std::optional formatter is defined in [include/fmt/std.h](https://github.com/fmtlib/fmt/blob/main/include/fmt/std.h) at lines 121-147, guarded by #ifdef __cpp_lib_optional to ensure availability only when the standard library supports optional types.

The implementation uses partial template specialization:

template <typename T, typename Char>
struct formatter<std::optional<T>, Char,
                 std::enable_if_t<is_formattable<T, Char>::value>> {
 private:
  formatter<std::remove_cv_t<T>, Char> underlying_;
  static constexpr string_view none = "none";
  static constexpr string_view optional = "optional(";

The class stores an underlying formatter for the contained type T. During the parse() phase, it delegates to the underlying formatter after potentially enabling debug format mode via detail::maybe_set_debug_format(underlying_, true).

In the format() method, the implementation checks engagement status:

  • If !opt, it writes the literal "none" using detail::write<Char>(ctx.out(), none).
  • If engaged, it writes "optional(", invokes underlying_.format(*opt, ctx) to render the contained value, and appends the closing parenthesis.

View the complete implementation: optional formatter source (lines 121-147).

Variant Formatter Implementation

The std::variant formatter begins at line 360 in the same header, activated by #ifdef __cpp_lib_variant. This implementation handles heterogeneous types through compile-time type list inspection.

Before formatting, the library verifies that all alternative types are formattable using the is_variant_formattable trait (lines 506-514). This trait uses parameter packs and std::conjunction to ensure type safety across the variant's type list.

The formatter specialization appears as:

template <typename Variant, typename Char>
struct formatter<Variant, Char,
                 std::enable_if_t<std::conjunction_v<
                     is_variant_like<Variant>,
                     detail::is_variant_formattable<Variant, Char>>>> {

The format() method (lines 369-388) writes the prefix "variant(", then uses std::visit to dispatch to the active alternative. For each visited value, it calls detail::write_escaped_alternative<Char>(out, v, ctx) to apply the appropriate formatter while handling nested types correctly. The implementation includes exception handling for the valueless state, writing "valueless by exception" if std::bad_variant_access is caught.

View the complete implementation: variant formatter source (lines 360-388).

Practical Usage Examples

To use these formatters, include the standard library extension header and use the standard fmt::format API:

#include <fmt/core.h>
#include <fmt/std.h>  // Required for std::optional/std::variant support
#include <optional>
#include <variant>
#include <string>

int main() {
    // Formatting std::optional
    std::optional<int> opt_val = 42;
    std::optional<int> opt_empty;
    
    fmt::print("Value: {}\n", opt_val);   // Output: Value: optional(42)
    fmt::print("Empty: {}\n", opt_empty); // Output: Empty: none
    
    // Formatting std::variant
    using Var = std::variant<int, double, std::string>;
    Var v1 = 3.14;
    Var v2 = std::string("fmt");
    
    fmt::print("Variant 1: {}\n", v1);  // Output: Variant 1: variant(3.14)
    fmt::print("Variant 2: {}\n", v2);  // Output: Variant 2: variant(fmt)
    
    // Nested types
    std::optional<Var> nested = Var{100};
    fmt::print("Nested: {}\n", nested);  // Output: Nested: optional(variant(100))
}

The formatters automatically handle debug format specifications when using the ? specifier, which quotes strings and escapes characters appropriately for the contained types.

Key Implementation Details

The standard library formatters in fmtlib demonstrate several architectural best practices:

  • Conditional Compilation: Formatters are wrapped in feature-test macros (__cpp_lib_optional, __cpp_lib_variant) to prevent compilation errors on older compilers or standard library implementations.
  • Recursive Formatting: Both formatters delegate to existing formatter specializations for their type parameters, enabling arbitrary nesting depth (e.g., std::optional<std::variant<std::optional<int>>>).
  • Exception Safety: The variant formatter explicitly catches std::bad_variant_access to handle degenerate valueless states gracefully.
  • Zero Overhead: The implementation uses constexpr parsing and type erasure only at the format context boundary, maintaining the library's performance characteristics.

Summary

  • Include <fmt/std.h> to enable formatting support for std::optional and std::variant in the fmt library.
  • The optional formatter outputs optional(value) for engaged optionals or none for empty states, utilizing underlying type formatters for recursive rendering.
  • The variant formatter uses std::visit to dispatch formatting to the active alternative, outputting variant(value) while handling valueless-by-exception states.
  • Both implementations reside in include/fmt/std.h and employ compile-time type checks via is_formattable traits to ensure type safety.
  • Nested standard library types format automatically through recursive formatter delegation without additional user code.

Frequently Asked Questions

Do I need to define custom formatters for std::optional or std::variant?

No, the fmt library provides built-in formatters for these types in the <fmt/std.h> header. As long as the contained types are formattable (either built-in or having custom formatter specializations), the optional and variant formatters work automatically without additional boilerplate.

How does fmtlib handle empty optionals or valueless variants?

The optional formatter outputs the string literal "none" when the optional is disengaged. For variants, the implementation catches std::bad_variant_access and outputs "valueless by exception" if the variant enters a degenerate state due to an exception during assignment.

Can I format nested types like std::optional<std::variant<int, std::string>>?

Yes, the formatters support arbitrary nesting because each delegates formatting to the underlying type's formatter specialization. The variant formatter uses detail::write_escaped_alternative which recursively applies the correct formatting logic to nested optionals, variants, or other standard containers.

What C++ standard version is required to use these formatters?

You need a compiler and standard library that support C++17 or later, as indicated by the feature test macros __cpp_lib_optional and __cpp_lib_variant that guard the formatter definitions in include/fmt/std.h. The fmt library itself maintains compatibility back to C++11, but these specific formatters require standard library components introduced in C++17.

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 →