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.,0xprefix 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
10or5 - 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 documentationsrc/format.cc— Core implementation containingdetail::parse_format_specsand thestateenumeration (≈ 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, anddone - 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 samewidthandprecisionstates - Source location: The enumeration lives in
src/format.ccwithindetail::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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →