How std::format Works in Modern C++
Introduced in C++20, std::format provides a fast,
type-safe, and extensible alternative to legacy formatting mechanisms
like printf and std::stringstream. By adopting
a syntax inspired by Python's str.format, it allows
developers to build formatted strings using replacement fields enclosed
in curly braces. This article explains the underlying mechanics of
format strings, including positional indexing, standard format
specifications for alignment and numbers, compile-time validation, and
custom type formatting.
Basic Syntax and Replacement Fields
At its simplest, a format string contains ordinary text mixed with
replacement fields denoted by {}. During execution,
std::format replaces each field with the string
representation of the corresponding argument passed to the function.
#include <format>
#include <iostream>
#include <string>
int main() {
std::string message = std::format("Hello, {}! You have {} new notifications.", "Alice", 5);
std::cout << message; // Output: Hello, Alice! You have 5 new notifications.
}Arguments are matched automatically from left to right. However, explicit positional indices can also be used to change the order of arguments or reuse them multiple times:
// Positional indexing
std::string repeated = std::format("{0} can be {1}, but {0} is always {0}.", "Code", "complex");
// Output: Code can be complex, but Code is always Code.Note: You cannot mix automatic and manual indexing within the same format string.
Format Specifiers
A replacement field can include an optional colon (:)
followed by a format specification: {index:specifier}. The
standard specification grammar supports alignment, width, precision, and
type presentation.
The general syntax follows this pattern:
[[fill]align][sign]['#']['0'][width]['.' precision]['L'][type]
Alignment and Width
You can control field width and text alignment using
<, >, or ^:
<: Left-aligns the result (default for non-numbers).>: Right-aligns the result (default for numbers).^: Centers the result.
An optional fill character can precede the alignment operator:
std::format("{:<10}", "left"); // "left "
std::format("{:>10}", "right"); // " right"
std::format("{:^10}", "center"); // " center "
std::format("{:*^10}", "fill"); // "***fill***"Numeric Formatting and Signs
Numbers can be formatted in different radices and notation styles:
- Integers:
d(decimal),x/X(hexadecimal),o(octal),b/B(binary). - Alternative Form (
#): Adds prefixes like0x,0b, or forces decimal points. - Signs:
+(always display sign),-(display only for negative, default),(space for positive).
std::format("{:#x}", 255); // "0xff"
std::format("{:#010b}", 5); // "0b00000101" (padded with zeros)
std::format("{:+d}", 42); // "+42"Floating-Point Precision
Floating-point presentation types include
f/F (fixed-point),
e/E (scientific),
g/G (general), and
a/A (hex-float). Precision is specified with a
period (.) followed by an integer:
double pi = 3.1415926535;
std::format("{:.2f}", pi); // "3.14"
std::format("{:10.4f}", pi); // " 3.1416"
std::format("{:.2e}", pi); // "3.14e+00"Compile-Time Format String Checking
Unlike C's printf, which performs type checking via
compiler warnings that can be bypassed or missed,
std::format verifies format strings at compile time using
consteval constructors.
If a format specifier does not match the provided argument type (for
example, attempting to format a string with {:d}), the code
fails to compile:
// This causes a compilation error:
// std::string bad = std::format("{:d}", "not a number");For dynamic format strings constructed at runtime,
std::vformat or std::runtime_format
(introduced in C++26) must be used explicitly.
Formatting User-Defined Types
The format string engine is extensible. To support custom classes or
structs, you specialize the std::formatter template for
your type:
struct Point {
int x;
int y;
};
template <>
struct std::formatter<Point> {
constexpr auto parse(std::format_parse_context& ctx) {
return ctx.begin(); // Simple parser: accepts default "{}"
}
auto format(const Point& p, std::format_context& ctx) const {
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
// Usage:
Point pt{10, 20};
std::string result = std::format("Point coordinates: {}", pt); // "Point coordinates: (10, 20)"By customizing parse() and format(), custom
types can process their own specifiers, integrating seamlessly into
standard format strings.