Common Formatting Specifiers in fmtlib: A Complete Guide to Type-Safe C++ Formatting

The most common formatting specifiers in fmtlib are d/i (signed decimal), u (unsigned), x/X (hexadecimal), b (binary), o (octal), f/F (fixed float), e/E (scientific), g/G (general), s (string), p (pointer), and c (character), combined with alignment (</>/^), width, precision, and sign controls.

The {fmt} library (commonly called fmtlib) provides a fast, type-safe alternative to C's printf and C++ streams. Its formatting syntax closely follows Python's str.format while adding C++-specific optimizations. This guide covers the core format specifiers defined in include/fmt/base.h and demonstrates their practical usage.

Core Format Specifiers for Integral Types

Integral types—int, unsigned, bool, char, and their variants—support multiple base representations. These specifiers are parsed in include/fmt/base.h around line 655.

Specifier Meaning
d, i Signed decimal integer
u Unsigned decimal integer
b Binary (base 2)
o Octal (base 8)
x Lowercase hexadecimal
X Uppercase hexadecimal
c Character (converts integral to char)
#include <fmt/format.h>

// Base conversions
fmt::print("Decimal: {}, Hex: {:x}, Binary: {:b}\n", 255, 255, 255);
// Output: Decimal: 255, Hex: ff, Binary: 11111111

// Uppercase hex and octal
fmt::print("Upper hex: {:X}, Octal: {:o}\n", 255, 64);
// Output: Upper hex: FF, Octal: 100

// Character conversion
fmt::print("ASCII 65 = {:c}\n", 65);  // Output: ASCII 65 = A

Floating-Point Format Specifiers

Floating-point formatting in fmtlib provides precise control over notation and precision. The specifiers mirror standard C conventions with extended functionality.

Specifier Meaning
f, F Fixed-point notation (e.g., 123.456)
e, E Scientific notation (e.g., 1.234560e+02)
g, G General format—uses e or f based on magnitude
a, A Hexadecimal floating-point
.N Precision: digits after decimal (or total digits for g)
double pi = 3.141592653589793;

// Fixed precision
fmt::print("{:.2f}\n", pi);     // 3.14

// Scientific notation
fmt::print("{:e}\n", pi);       // 3.141593e+00

// General format auto-selects
fmt::print("{:g}\n", 1000000.0);  // 1e+06
fmt::print("{:g}\n", 0.0001);     // 0.0001

// Hexadecimal float
fmt::print("{:a}\n", pi);       // 0x1.921fb54442d18p+1

String and Pointer Specifiers

Strings and pointers have dedicated specifiers with special behavior for width and precision.

Specifier Meaning
s String (default for std::string, char*, string_view)
p Pointer address (implementation-defined format)
? Debug representation (calls custom formatter)
const char* cstr = "hello";
std::string cppstr = "world";
int value = 42;

// Basic string formatting
fmt::print("{:s} {:s}\n", cstr, cppstr);  // hello world

// Pointer address
fmt::print("{:p}\n", &value);  // 0x7ffd3e8c2a4c (example)

// Precision truncates strings
fmt::print("{:.3}\n", "substring");  // sub

Alignment, Width, and Fill Options

The format specification mini-language supports flexible layout control through alignment and width specifiers.

Component Syntax Meaning
Fill character Any character before alignment Pad with this character
Alignment < (left), > (right), ^ (center) Position within field
Width Number or * Minimum field width
Precision .N or .* Digits after decimal or max string length
// Right-aligned (default for numbers)
fmt::print("[{:>10}]\n", 42);     // [        42]

// Left-aligned (default for strings)
fmt::print("[{:<10}]\n", "hi");   // [hi        ]

// Centered
fmt::print("[{:^10}]\n", "mid");  // [   mid    ]

// Custom fill character
fmt::print("[{:*>10}]\n", 42);    // [********42]

// Combined: zero-padding for numbers
fmt::print("[{:08}]\n", 42);      // [00000042]

Sign, Hash, and Locale Modifiers

Additional flags modify numeric presentation for specialized output requirements.

Flag Effect
+ Always show sign (+42, -7)
- Only negative sign (default)
(space) Space for positive, minus for negative
# Alternate form: 0x prefix for hex, 0 for octal, decimal point for floats
L Use locale-specific thousands separator
// Sign control
fmt::print("{:+d} {:+d}\n", 42, -7);   // +42 -7
fmt::print("{: d} {: d}\n", 42, -7);   //  42 -7

// Alternate form
fmt::print("{:#x}\n", 255);            // 0xff
fmt::print("{:#o}\n", 8);              // 010
fmt::print("{:#.0f}\n", 1000.0);       // 1000.

// Locale-aware formatting (requires locale support)
fmt::print("{:L}\n", 1234567);         // 1,234,567 (with appropriate locale)

Dynamic Width and Precision

Fmtlib supports runtime-specified widths and precisions using * substitution, evaluated from subsequent arguments.

int width = 10;
int precision = 3;
double value = 3.14159;

// Dynamic width
fmt::print("[{:{}}]\n", 42, width);           // [        42]

// Dynamic precision
fmt::print("[{:.{}}]\n", value, precision);   // [3.142]

// Both dynamic
fmt::print("[{:{}}]\n", value, width);        // 3.14159 (no precision specified)

// Common pattern: table alignment
std::vector<std::pair<std::string, int>> data = {
    {"Alice", 1500},
    {"Bob", 25000},
    {"Charlie", 300}
};

int col_width = 10;
for (auto& [name, score] : data) {
    fmt::print("{:<{}} | {:>{}}\n", name, col_width, score, col_width);
}
// Alice      |       1500
// Bob        |      25000
// Charlie    |        300

Range Formatting with Custom Specifiers

The include/fmt/ranges.h header extends specifier support to containers and ranges through fmt::join and automatic range formatting.

#include <fmt/ranges.h>
#include <vector>
#include <map>

std::vector<int> nums = {1, 2, 3, 4};

// Default range formatting
fmt::print("{}\n", nums);  // [1, 2, 3, 4]

// Custom element formatting via join
fmt::print("{}\n", fmt::join(nums, " | "));  // 1 | 2 | 3 | 4

// Specifier applied to each element
fmt::print("{::04x}\n", nums);  // [0x0001, 0x0002, 0x0003, 0x0004]

// Nested containers
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4}};
fmt::print("{}\n", nested);  // [[1, 2], [3, 4]]

Complete Format Specification Syntax

The full grammar for a replacement field in include/fmt/base.h follows this structure:


{[arg_id][:[][fill]align][sign][#][0][width][.precision][type]}

Where:

  • arg_id — explicit argument index (e.g., {0}, {1})
  • fill — any character except { or }
  • align — < (left), > (right), ^ (center), = (numeric, pad after sign)
  • sign — +, -, or (space)
  • # — alternate form flag
  • 0 — zero-fill (equivalent to fill=0, align==)
  • width — integer or * for dynamic
  • precision — . followed by integer or * for dynamic
  • type — format specifier from tables above
// Complex specification combining multiple features
fmt::print("{:+#08.2f}\n", 3.14159);  // +003.14

// Breakdown: +: always show sign
//            #: alternate form (ensures decimal point)
//            0: zero-pad
//            8: total width
//            .2: 2 digits after decimal
//            f: fixed-point

Summary

  • Integral specifiers: d/i, u, b, o, x/X, c control base and character conversion
  • Floating-point specifiers: f/F, e/E, g/G, a/A select notation; precision controls digits
  • Layout controls: </>/^ alignment, custom fill, width, and .precision manage field sizing
  • Numeric flags: +/-/ for signs, # for alternate forms, 0 for zero-padding, L for locale
  • Dynamic values: * in width or precision position enables runtime specification
  • Range support: include/fmt/ranges.h extends specifiers to containers with fmt::join and :: syntax

Frequently Asked Questions

What is the difference between d and i specifiers in fmtlib?

Both d and i produce identical signed decimal output in fmtlib. The duplication exists for compatibility with printf conventions—d is the standard decimal specifier, while i matches scanf's interpretation. In practice, always use d for clarity: fmt::print("{:d}", -42) produces -42.

How do I left-pad a number with zeros in fmtlib?

Use the 0 flag or explicit fill-align syntax. The specifier {:08} produces 00000123 for input 123. This is equivalent to {:0>8}—zero fill, right alignment, width 8. For numbers with signs, use {:0=8} to pad between sign and digits, giving -0000123 for -123.

Can I use fmtlib specifiers with printf-style functions?

Yes, through include/fmt/printf.h. The fmt::printf() and fmt::sprintf() functions accept standard printf format strings, but internally map to fmtlib's type-safe engine. Specifiers like %d, %x, %f work directly, though you lose compile-time type checking compared to fmt::format().

Why does my custom type need the ? specifier?

The ? specifier invokes debug formatting via your fmt::formatter<T> specialization. It typically outputs a representation useful for debugging—often with type information or escaped strings. For standard types, ? is not required; for custom types without explicit formatter support, it triggers the custom formatter if available.

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 →