std::format — Python-Style Formatting in C++
C++20 brought std::format — a type-safe, extensible, and readable string formatting library that finally replaces both printf and the verbose stream-insertion syntax. C++23 built on it with std::print and std::println, adding direct console output with superior Unicode support and better performance. If you've used Python's f-strings or Rust's format!, the mental model transfers immediately.
Why a new formatting library?
Before C++20, you had two choices for string formatting, each with a fatal flaw. C-style printf is concise and readable — the format string is clearly separated from its arguments — but it is not type-safe (passing a double for a %d specifier is undefined behavior) and cannot be extended to support your own types. The C++ stream approach (std::cout <<) is type-safe and extensible via operator<<, but the format string and arguments are interleaved, making it hard to read and nearly impossible to localize for different languages. std::format is the best of both worlds.
C-style printf — not type-safe
printf("x=%d y=%d\n", x, y);
// passing wrong type → UBI/O streams — hard to read
cout << "x=" << x
<< " y=" << y
<< endl;std::format — best of both
println("x={} y={}", x, y);
// type-safe, readable, extensibleBasic usage: format, print, println
std::format(), defined in <format>, returns a formatted std::string. Its C++23 siblings std::print() and std::println() (in <print>) write directly to an output stream — by default stdout, the same destination as std::cout, but with better Unicode support and better performance. println appends a newline; print does not. When you can use C++23, prefer println over std::format + cout.
import std; // or: #include <format> and #include <print>
int x { 42 };
double pi { 3.14159 };
std::string name { "Alice" };
// std::format returns a std::string
std::string s = std::format("x={} pi={} name={}", x, pi, name);
// s == "x=42 pi=3.14159 name=Alice"
// std::print / std::println (C++23) write to stdout directly
std::println("x={} pi={} name={}", x, pi, name);
// output: x=42 pi=3.14159 name=Alice
// Print to stderr
std::println(std::cerr, "Error: value {} is out of range", x);std::string as the format string is a compile error. For runtime format strings (e.g., loaded from a translation file), use std::vprint_unicode() with std::make_format_args().Replacement fields and argument indices
Every {} in a format string is a replacement field. The full syntax for a replacement field is {[index][:specifier]}. When you omit the index, arguments are consumed left-to-right. When you specify an index (zero-based), you can reorder, repeat, or skip arguments — which is essential for internationalization, where different languages may use the same values in a different order. You cannot mix automatic and manual indexing in the same format string. Literal { and } characters are escaped by doubling them: {{ and }}.
int n { 42 };
std::string file { "data.txt" };
// Automatic indexing (left-to-right)
std::println("Read {} bytes from {}", n, file);
// → Read 42 bytes from data.txt
// Manual indexing — same result, arguments explicitly addressed
std::println("Read {0} bytes from {1}", n, file);
// Reordered for Chinese (argument order unchanged, format order swapped)
std::println("从{1}中读取{0}个字节。", n, file);
// → 从data.txt中读取42个字节。
// Repeat an argument
std::println("{0} × {0} = {1}", 7, 49);
// → 7 × 7 = 49
// Escape literal braces
std::println("Set notation: {{{}}}", 42);
// → Set notation: {42}Format specifiers
After the colon inside a replacement field comes the format specifier, which controls precisely how the value is rendered. The general grammar is:
{ [index] : [[fill]align] [sign] [#] [0] [width] [.precision] [L] [type] }All parts are optional and can be combined in any subset.
Width and alignment
width sets the minimum field width. Without an alignment specifier, integers and floats default to right-aligned (>); strings default to left-aligned (<). Center alignment uses ^. The optional fill character precedes the alignment character; if omitted, spaces are used. Width can also be dynamic — specified as a nested replacement field.
int i { 42 };
std::println("|{:5}|", i); // | 42| (right-aligned, width 5)
std::println("|{:<7}|", i); // |42 | (left-aligned, width 7)
std::println("|{:^7}|", i); // | 42 | (center-aligned, width 7)
std::println("|{:_>7}|", i); // |_____42| (fill with '_', right-align)
std::println("|{:_^10}|", i); // |____42____| (fill with '_', center)
// Dynamic width — taken from argument
int width { 10 };
std::println("|{:{}}|", i, width); // | 42|
std::println("|{1:{0}}|", width, i); // | 42| (indexed)
// String with fill — useful for simple table borders
std::println("|{:=>16}|", ""); // |================|Sign
The sign specifier controls whether a sign character is shown before numeric values. Three options: - (default — sign only for negatives), + (sign for both positive and negative), and (space for positive, minus for negative — useful for aligning columns of mixed-sign numbers).
int i { 42 };
std::println("|{:5}|", i); // | 42| (default: no + sign)
std::println("|{:+5}|", i); // | +42| (always show sign)
std::println("|{:< 5}|", i); // | 42| (space for positive)
std::println("|{:< 5}|", -i); // | -42| (minus for negative)Type specifier and alternate format (#)
The type letter controls the representation. For integers: d (decimal, default), b/B (binary), o (octal), x/X (hexadecimal). The # flag enables alternate formatting: for hex it prepends 0x/0X, for binary 0b/0B, for octal 0. For floats, # forces the decimal point even if no fractional digits follow.
int i { 42 };
std::println("|{:10d}|", i); // | 42| decimal (default)
std::println("|{:10b}|", i); // | 101010| binary
std::println("|{:#10b}|", i); // | 0b101010| binary with 0b prefix
std::println("|{:10X}|", i); // | 2A| uppercase hex
std::println("|{:#10X}|", i); // | 0X2A| hex with 0X prefix
// String centering
std::string s { "ProCpp" };
std::println("|{:_^10}|", s); // |__ProCpp__|
// Boolean
bool b { true };
std::println("{}", b); // true (default: string form)
std::println("{:d}", b); // 1 (integer form)Precision
Precision is specified as .N where N is the number of significant digits for floating-point (or digits after the decimal point for f/Fformat) or the maximum number of characters for strings. Like width, precision can be dynamic via a nested replacement field.
double d { 3.1415 / 2.3 };
std::println("|{:12}|", d); // | 1.36587| (default)
std::println("|{:12.2}|", d); // | 1.4| (2 sig digits)
std::println("|{:12.2f}|", d); // | 1.37| (2 decimal places, fixed)
std::println("|{:12e}|", d); // |1.365870e+00| (scientific)
// Dynamic precision
int width { 12 }, prec { 3 };
std::println("|{2:{0}.{1}f}|", width, prec, d); // | 1.366|Zero-fill and locale (L)
The 0 flag inserts leading zeros to reach the specified width. Unlike a fill character, zeros appear after the sign and after any 0x/0b prefix. The L flag enables locale-specific formatting (digit group separators, decimal point characters) — it requires passing a std::locale as the first argument to std::format(), and is not supported by print()/println().
int i { 42 };
std::println("|{:06d}|", i); // |000042|
std::println("|{:+06d}|", i); // |+00042| (sign before zeros)
std::println("|{:#06X}|", i); // |0X002A| (prefix, then zeros)
// Locale-specific (format only, not println)
float f { 1234.5f };
std::cout << std::format(std::locale{"nl"}, "{:Lg}\n", f);
// → 1.234,5 (Dutch locale: period as thousands sep, comma as decimal)Formatting ranges (C++23)
C++23 allows formatting containers and ranges directly. A vector, array, pair, or any range can be passed as an argument to format()/println() without writing a loop. By default, sequences are surrounded by square brackets with comma-separated elements; pairs use parentheses. The full range specifier grammar adds control over surrounding brackets, per-element formatting, and string-like output.
std::vector values { 11, 22, 33 };
std::println("{}", values); // [11, 22, 33] (default)
std::println("{:n}", values); // 11, 22, 33 (n = omit brackets)
std::println("{{{:n}}}", values); // {11, 22, 33} (custom brackets via escaped {})
// Range width/fill applies to the whole range output
std::println("{:*^16}", values); // **[11, 22, 33]**
std::println("{:*^16n}", values); // ***11, 22, 33***
// Per-element specifier — nested after ::
std::println("{::^6}", values); // [ 11 , 22 , 33 ] (each element centered width 6)
std::println("{:n:*^6}", values); // **11**, **22**, **33** (n + per-element)// Formatting pairs (default: parentheses, comma-separated)
std::pair p { 11, 22 };
std::println("{}", p); // (11, 22)
std::println("{:n}", p); // 11, 22
std::println("{:m}", p); // 11: 22 (m = map-like key: value format)// Formatting strings as ranges of characters
std::vector<char> chars { 'W','o','r','l','d','\t','!' };
std::println("{}", chars); // ['W', 'o', 'r', 'l', 'd', '\t', '!']
std::println("{:s}", chars); // World ! (string form, no escaping)
std::println("{:?s}", chars); // "World\t!" (escaped string form)Escaped string/char output (C++23)
The ? type specifier formats strings and characters as they would appear in C++ source code — surrounded by quotes and with control characters escaped. This is primarily useful for debugging and logging, where you need to see invisible characters (tabs, newlines) literally.
std::println("|{:?}|", "Hello\tWorld!\n"); // |"Hello\tWorld!\n"|
std::println("|{:?}|", "\"quoted\""); // |"\"quoted\""|
std::println("|{:?}|", '\t'); // |'\t'|
std::println("|{:?}|", '''); // |'\''|Custom type support
The formatting library is extensible: any type can be made formattable by specializing std::formatter<T> in the std namespace. The specialization needs two member functions: parse(), which reads the format specifier substring from the format string, and format(), which writes the formatted value to the output context. Once written, the specialization integrates seamlessly — your type works with all the standard specifiers you compose in the formatter, and supports nested format specs just like built-in types. The next lesson covers writing custom formatters in detail.
struct Point { int x, y; };
template <>
class std::formatter<Point>
{
public:
constexpr auto parse(auto& context) {
return context.begin(); // no custom specifiers for now
}
auto format(const Point& p, auto& ctx) const {
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
int main()
{
Point pt { 3, 7 };
std::println("Point: {}", pt); // Point: (3, 7)
std::println("Centered: {:^12}", pt); // Centered: (3, 7)
std::vector<Point> pts { {1,2}, {3,4} };
std::println("{}", pts); // [(1, 2), (3, 4)]
}Specifier quick reference
| Specifier | Meaning | Example | Output |
|---|---|---|---|
| :5 | Min width 5 | {:5}, 42 | 42 |
| :<8 | Left-align, width 8 | {:<8}, 42 | 42 |
| :.^8 | Center, dot fill | :.^8}, 42 | ...42... |
| :+d | Show + for positives | {:+d}, 42 | +42 |
| :06d | Zero-pad to 6 | {:06d}, 42 | 000042 |
| :#x | Hex with 0x prefix | {:#x}, 42 | 0x2a |
| :#010b | Binary, 0b prefix, width 10 | {:#010b}, 42 | 0b00101010 |
| :.3f | 3 decimal places | {:.3f}, 3.14159 | 3.142 |
| :.2e | Scientific, 2 decimal | {:.2e}, 3.14159 | 3.14e+00 |
| :? | Escaped string/char (C++23) | '{:?}', 'hi\n' | "hi\n" |
| :n (range) | Omit outer brackets (C++23) | {:n}, vec | 1, 2, 3 |
| ::spec (range) | Per-element format (C++23) | '{::>4}', vec | [ 1, 2, 3] |