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 flag0— zero-fill (equivalent to fill=0, align==)width— integer or*for dynamicprecision—.followed by integer or*for dynamictype— 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,ccontrol base and character conversion - Floating-point specifiers:
f/F,e/E,g/G,a/Aselect notation; precision controls digits - Layout controls:
</>/^alignment, custom fill,width, and.precisionmanage field sizing - Numeric flags:
+/-/for signs,#for alternate forms,0for zero-padding,Lfor locale - Dynamic values:
*in width or precision position enables runtime specification - Range support:
include/fmt/ranges.hextends specifiers to containers withfmt::joinand::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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →