fmtlib Format Specifier Parsing States: Inside the State Machine Implementation

fmtlib uses a six-state machine (arg_id, flags, width, precision, type, done) to parse format specifiers, implemented in detail::parse_format_specs in src/format.cc.

The {fmt} library—one of the fastest and most widely-used C++ formatting libraries—processes format strings through a compact hand-crafted state machine. Understanding these states helps developers write more efficient format strings and debug parsing errors. This article breaks down each parsing state exactly as implemented in the fmtlib source code.

The Six Parsing States in fmtlib's State Machine

The state machine is defined in src/format.cc around line 1040, within the detail::parse_format_spec enumeration. Here are the states in their logical order of progression:

State Purpose Typical Input
arg_id Parse optional argument index or named identifier 0, name
flags Parse format flags +, -, #, 0, space
width Parse field width specification 10, {width}
precision Parse precision after decimal point 2, {prec}
type Parse conversion type specifier d, f, x, g
done Final state when closing } is reached }

The core enumeration appears in simplified form as:

enum class state {
  arg_id,      // reading the argument identifier
  flags,       // +, -, #, 0, space
  width,       // field width
  precision,   // "." followed by precision value
  type,        // conversion type
  done         // '}' reached
};

This state machine drives the detail::parse_format_specs function, which iterates through characters following the colon in a replacement field.

State-by-State Breakdown

arg_id State

The parser enters this state immediately after the opening { or after a colon : if an argument identifier was present. It handles:

  • Numeric indices: {0}, {1}, {42}
  • Named arguments: {name}, {value}

If no identifier is present, the parser advances to the next argument automatically.

flags State

Once past the argument identifier, the parser collects format flags in any combination:

  • + — always show sign
  • - — left-align
  • # — alternate form (e.g., 0x prefix for hex)
  • 0 — zero-padding
  • space — leading space for positive numbers

From src/format.cc, the flag parsing logic consumes these characters before transitioning to width detection.

width State

The width state accepts:

  • Static widths: Decimal numbers like 10 or 5
  • Dynamic widths: Nested replacement fields like {width} or {} (for next argument)

The parser distinguishes these by checking for an opening { character.

precision State

Triggered only when a dot . is encountered. Similar to width, this accepts:

  • Static precision: .{2}, .{6}
  • Dynamic precision: .{prec}, .{3} (argument index)

This state is skipped entirely if no dot appears before the type specifier.

type State

The type state consumes the conversion specifier—essentially mandatory for built-in types. Supported types include:

  • Integer: d, i, u, b, B, o, x, X
  • Floating-point: f, F, e, E, g, G, a, A
  • String/char: s, c
  • Pointer: p
  • Special: ? (debug), n (count), % (literal percent)

User-defined formatters may extend this set.

done State

Reached when the closing } is encountered. At this point, parse_format_specs returns the fully populated format specification structure.

Practical Code Examples

Each example demonstrates state transitions through the machine:

#include <fmt/core.h>

// Example 1: Integer with flags and width
// States: arg_id (implicit) → flags (+, 0) → width (8) → type (d)
fmt::print("{:+08d}\n", 42);
// Output: +0000042

// Example 2: Float with alignment, width, precision
// States: arg_id → flags (>) → width (10) → precision (3) → type (f)
fmt::print("{:>10.3f}\n", 3.14159);
// Output: "     3.142"

// Example 3: Named arguments with dynamic width/precision
// States: arg_id (name) → flags (>) → width ({w}) → precision ({p}) → type (g)
fmt::print("{name:>{w}.{p}g}\n",
           fmt::arg("name", 12345.6),
           fmt::arg("w", 12),
           fmt::arg("p", 2));
// Output: "      1.2e+04"

Key Implementation Files

The fmtlib format specifier parsing state machine spans these critical files:

  • include/fmt/format.h — Public API and format specification grammar documentation
  • src/format.cc — Core implementation containing detail::parse_format_specs and the state enumeration (≈ line 1040)
  • test/format-test.cc — Comprehensive tests exercising each state transition

Summary

  • Six states form fmtlib's format specifier parser: arg_id, flags, width, precision, type, and done
  • State transitions are deterministic and follow the syntax [arg_id]:[flags][width][.precision][type]
  • Dynamic specifications (runtime width/precision) use nested {} syntax, parsed in the same width and precision states
  • Source location: The enumeration lives in src/format.cc within detail::parse_format_specs

Frequently Asked Questions

How does fmtlib handle invalid state transitions?

The parser throws format_error with descriptive messages. For example, placing a flag after width ({5+}) fails in src/format.cc when the state machine encounters an unexpected character for the current state.

Can user-defined types add custom parsing states?

No—the six-state machine is fixed. However, parse() member functions in custom formatters receive the remaining format string after type and can implement sub-parsers. The built-in states are intentionally minimal for performance.

What is the performance impact of dynamic width/precision?

Minimal. According to the src/format.cc implementation, dynamic specs ({:{}}) simply read another argument during the width or precision state—no additional virtual calls or heap allocations occur.

Where is the state machine actually invoked?

detail::parse_format_specs is called from format_handler::on_format_specs in include/fmt/format.h, triggered when the format string parser encounters a colon : in a replacement field.

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 →