Skip to content
C++

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 → UB

I/O streams — hard to read

cout << "x=" << x
     << " y=" << y
     << endl;

std::format — best of both

println("x={} y={}", x, y);
// type-safe, readable, extensible

Basic 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);
C++23 compile-time check: The format string must be a compile-time constant so that syntax errors are caught at compile time. Passing a runtime 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

SpecifierMeaningExampleOutput
:5Min width 5{:5}, 42 42
:<8Left-align, width 8{:<8}, 4242
:.^8Center, dot fill:.^8}, 42...42...
:+dShow + for positives{:+d}, 42+42
:06dZero-pad to 6{:06d}, 42000042
:#xHex with 0x prefix{:#x}, 420x2a
:#010bBinary, 0b prefix, width 10{:#010b}, 420b00101010
:.3f3 decimal places{:.3f}, 3.141593.142
:.2eScientific, 2 decimal{:.2e}, 3.141593.14e+00
:?Escaped string/char (C++23)'{:?}', 'hi\n'"hi\n"
:n (range)Omit outer brackets (C++23){:n}, vec1, 2, 3
::spec (range)Per-element format (C++23)'{::>4}', vec[ 1, 2, 3]
← The #include problem modules solveNext: Custom formatters for user-defined types — coming soon
Sign in to track progress