std::format and std::print
C++20 std::format and C++23 std::print — type-safe formatted output with compile-time checking, custom formatters, and efficient direct printing.
std::formatsince C++20A compile-time-checked, type-safe function that formats arguments into a string according to a format specifier, replacing sprintf and ostringstream for most use cases.
Overview
std::format (C++20) formats values into a std::string with compile-time validation of the format string and argument count. std::print and std::println (C++23) perform the same formatting but write directly to an output stream, eliminating the intermediate string allocation.
Key advantages over legacy approaches:
- Compile-time safety: Format specifiers and argument counts are checked at compile time (if the format string is a string literal)
- Type-safe: Unlike
sprintf, format specifiers cannot mismatch their arguments - Locale-independent by default: No
setlocaleside effects; use:Lto enable locale-aware formatting - Performance: Significantly faster than
sprintfandostringstreamfor typical formatting tasks - Familiar syntax: Similar to Python's
str.format()and JavaScript template literals
Syntax
Basic Format String
#include <format> // C++20
#include <print> // C++23
std::string s = std::format(format_string, arg0, arg1, ...);
std::print(format_string, arg0, arg1, ...); // C++23
std::println(format_string, arg0, arg1, ...); // C++23; adds newlineFormat Specification Mini-Language
{[arg-id][:[fill][align][sign][#][0][width][grouping][.precision][type]]}- arg-id:
0,1,2, ... or omitted for sequential - fill/align: padding character and alignment (
<,>,^for left, right, center) - sign:
+(always show),-(negative only, default), space (space for positive) #: alternate form (prefix0x,0b,0ofor integers; decimal point for floats)0: zero-pad (shorthand forfill='0'withalign='0')- width: minimum field width
- grouping:
,for thousands separators (C++26; use custom formatter for C++20–23) - precision: decimal places (floats) or max width (strings)
- type:
d,b,o,x,X(integers);f,e,E,g,G(floats);s(strings);c(char);B,b(bool)
Examples
Positional and Sequential Arguments
// Sequential (automatic indexing)
std::format("{} + {} = {}", 1, 2, 3); // "1 + 2 = 3"
// Positional indexing
std::format("{2} {0} {1}", "a", "b", "c"); // "c a b"
// Reuse arguments
std::format("{0} {0}", "hello"); // "hello hello"Integer Formatting
std::format("{}", 42); // "42"
std::format("{:d}", 42); // "42" (decimal, explicit)
std::format("{:b}", 42); // "101010" (binary; C++20)
std::format("{:o}", 42); // "52" (octal)
std::format("{:x}", 255); // "ff" (hex lowercase)
std::format("{:X}", 255); // "FF" (hex uppercase)
std::format("{:#x}", 0xFF); // "0xff" (alternate form)
std::format("{:#b}", 42); // "0b101010" (alternate form)
std::format("{:+d}", 42); // "+42" (always show sign)
std::format("{:010d}", 42); // "0000000042" (zero-padded width 10)
std::format("{:_^10}", "x"); // "____x_____" (custom fill char)Float Formatting
std::format("{}", 3.14159); // "3.14159" (shortest round-trip)
std::format("{:f}", 3.14); // "3.140000" (fixed, 6 decimals)
std::format("{:.2f}", 3.14159); // "3.14" (fixed, 2 decimals)
std::format("{:e}", 3.14); // "3.140000e+00" (scientific)
std::format("{:.2e}", 3.14); // "3.14e+00"
std::format("{:g}", 3.14); // "3.14" (shortest of f or e)
std::format("{:g}", 1e15); // "1e+15" (switches to e for large values)
std::format("{:8.3f}", 3.14); // " 3.140" (width 8, 3 decimals, right-aligned)
std::format("{:<8.3f}", 3.14); // "3.140 " (left-aligned)
std::format("{:+.1f}", 3.14); // "+3.1" (show sign)String and Character Formatting
std::format("{}", "hello"); // "hello"
std::format("{:10}", "hi"); // "hi " (left-aligned, width 10)
std::format("{:>10}", "hi"); // " hi" (right-aligned)
std::format("{:.5}", "hello world"); // "hello" (truncate to 5 chars)
std::format("{:>10.3}", "hello"); // " hel" (right-align, width 10, max 3 chars)
std::format("{:c}", 65); // "A" (int to char)
std::format("{:b}", true); // "true" (boolean)
std::format("{:s}", true); // "1" (boolean as integer; avoid, use :b or :d)std::print and std::println (C++23)
#include <print>
std::print("Value: {}\n", 42); // writes to stdout, no newline added
std::println("Value: {}", 42); // writes to stdout, adds '\n'
std::println(std::cerr, "Error: {}", msg); // write to stderr
std::println(stdout, "C-style: {}", 42); // write to FILE*
// Direct printing is faster than std::format + std::cout
// because it avoids intermediate string allocationRuntime Format Strings
// std::format checks format string at compile time only if it's a string literal
std::string user_format = "{} + {} = {}";
std::string result = std::vformat(user_format, std::make_format_args(1, 2, 3));Custom Formatters
struct Point { double x, y; };
template<>
struct std::formatter<Point> {
bool use_polar = false;
constexpr auto parse(std::format_parse_context& ctx) {
auto it = ctx.begin();
if (it != ctx.end() && *it == 'p') {
use_polar = true;
++it;
}
return it;
}
auto format(const Point& p, std::format_context& ctx) const {
if (use_polar) {
double r = std::sqrt(p.x * p.x + p.y * p.y);
double theta = std::atan2(p.y, p.x);
return std::format_to(ctx.out(), "({:.2f}∠{:.2f}°)", r, theta * 180 / M_PI);
} else {
return std::format_to(ctx.out(), "({:.2f}, {:.2f})", p.x, p.y);
}
}
};
Point p{3.0, 4.0};
std::println("{}", p); // "(3.00, 4.00)"
std::println("{:p}", p); // "(5.00∠53.13°)"Best Practices
-
Prefer
std::printoverstd::format+std::cout(C++23): Direct output is faster and avoids intermediate string allocation. -
Use compile-time format strings: String literals enable compile-time format checking. For runtime strings, use
std::vformatand document why. -
Avoid locale-aware formatting unless needed: The default locale-independent formatting is faster and more predictable. Use
:Lonly when explicitly required for user-facing output. -
For custom types, specialize
std::formatter: More efficient and consistent than operator overloading withoperator<<. -
Specify precision for floats explicitly: Default precision can vary; use
.2for.3gto be clear about output format.
Common Pitfalls
Compile-time checking requires string literal format:
std::format("{}", 42); // ✓ OK, compiles with check
std::string fmt = "{}";
std::vformat(fmt, std::make_format_args(42)); // ✓ OK, but no compile-time checkFormat specifier mismatch is a compile error:
// std::format("{:d}", "hello"); // ✗ compile error: d is not valid for stringBoolean formatting defaults to 1/0 with {:d}:
std::format("{}", true); // "true" (default)
std::format("{:d}", true); // "1" (integer format)
std::format("{:b}", true); // "true" (explicit boolean)Precision truncates strings, not rounds:
std::format("{:.3}", "hello"); // "hel" (truncates to 3 characters)
std::format("{:.2f}", 3.999); // "4.00" (rounds for floats only)Locale-independent by default—use :L for locale-aware output:
std::format("{:,}", 1000000); // "1000000" (no separator, format ignores locale)
std::format("{:L}", 1000000); // locale-aware, e.g., "1,000,000" in en_US (C++26)Custom Formatter Specialization (Deep Dive)
The std::formatter protocol has exactly two member functions you must provide: parse and format. Understanding what each one does is the key to writing correct, composable custom formatters.
The Protocol
template <typename CharT>
struct std::formatter<MyType, CharT> {
// parse() is called at compile time (constexpr) to read the
// format-spec portion between ':' and '}'.
// It must return an iterator pointing past the last character it consumed.
constexpr auto parse(std::basic_format_parse_context<CharT>& ctx)
-> decltype(ctx.begin());
// format() is called at runtime to write formatted output.
// ctx.out() is an output iterator; write to it via std::format_to.
// Must return the iterator after the last character written.
auto format(const MyType& value,
std::basic_format_context<...>& ctx) const
-> decltype(ctx.out());
};parse receives a view of the format spec (the text after : inside the replacement field). It must advance the iterator past every character it recognizes and stop before the closing }. If the spec is empty, ctx.begin() == ctx.end() and you just return ctx.begin().
Complete Working Example: Point with Delegated Formatting
This example shows the full formatter life cycle for a 2-D point type. It also demonstrates delegating to an existing formatter so the user can apply any float spec (width, precision, sign) to both coordinates:
#include <format>
#include <cmath>
#include <numbers> // std::numbers::pi (C++20)
struct Point { double x, y; };
template <typename CharT>
struct std::formatter<Point, CharT> {
private:
// Reuse the standard double formatter to handle the full float spec.
std::formatter<double, CharT> float_fmt_;
bool polar_ = false;
public:
// parse() reads an optional leading 'p' for polar output, then
// delegates the rest of the spec to the inner float formatter.
constexpr auto parse(std::basic_format_parse_context<CharT>& ctx) {
auto it = ctx.begin();
if (it != ctx.end() && *it == 'p') {
polar_ = true;
++it;
}
// Reconstruct a sub-context starting at 'it' so the inner
// formatter can parse its own spec (e.g. ".2f").
// The simplest portable approach: set ctx's begin to 'it'.
// In practice, pass the remaining range to float_fmt_.parse:
ctx.advance_to(it);
return float_fmt_.parse(ctx);
}
auto format(const Point& p, std::format_context& ctx) const {
auto out = ctx.out();
if (polar_) {
double r = std::sqrt(p.x * p.x + p.y * p.y);
double theta = std::atan2(p.y, p.x) * 180.0 / std::numbers::pi;
out = std::format_to(out, "(r=");
ctx.advance_to(out);
out = float_fmt_.format(r, ctx);
out = std::format_to(out, ", θ=");
ctx.advance_to(out);
out = float_fmt_.format(theta, ctx);
return std::format_to(out, "°)");
} else {
out = std::format_to(out, "(");
ctx.advance_to(out);
out = float_fmt_.format(p.x, ctx);
out = std::format_to(out, ", ");
ctx.advance_to(out);
out = float_fmt_.format(p.y, ctx);
return std::format_to(out, ")");
}
}
};
// Usage
Point p{3.0, 4.0};
std::println("{:.2f}", p); // "(3.00, 4.00)"
std::println("{:p.1f}", p); // "(r=5.0, θ=53.1°)"
std::println("{:p.3f}", p); // "(r=5.000, θ=53.130°)"Formatter Constraints and Rules
parsemust beconstexprso format strings can be checked at compile time.formatmust beconst— the formatter object is shared across format calls.- Never throw from
parse; throwstd::format_errorfromformatif a value is unrepresentable. - The partial specialization
std::formatter<MyType>(no second parameter) is equivalent tostd::formatter<MyType, char>. For wide-string support also specialize onwchar_t.
Format Spec Mini-Language: Complete Reference
The format specification sits between the : and } of a replacement field. Every field is optional:
{ [arg-id] [: [fill] [align] [sign] [#] [0] [width] [.precision] [type] ] }Fill and Align
[fill][align] sets the padding character and direction. align is one of:
| Align | Meaning | Default for |
|---|---|---|
< | Left-align, pad on right | strings, booleans |
> | Right-align, pad on left | numbers |
^ | Center, equal padding both sides | — |
fill is any character that precedes the align character. If omitted, space is used.
std::format("{:*<10}", "hi"); // "hi********"
std::format("{:*>10}", "hi"); // "********hi"
std::format("{:*^10}", "hi"); // "****hi****"
std::format("{:_^15}", "center"); // "____center_____"Sign
| Spec | Meaning |
|---|---|
+ | Always show sign (+42, -42) |
- | Show sign only for negatives (default) |
| space | Space for positives, - for negatives ( 42, -42) — useful for aligning columns |
std::format("{:+}", 42); // "+42"
std::format("{:+}", -42); // "-42"
std::format("{: }", 42); // " 42"
std::format("{: }", -42); // "-42"Alternate Form (#)
Adds a base prefix for integers and forces a decimal point for floats:
std::format("{:#x}", 255); // "0xff"
std::format("{:#X}", 255); // "0XFF"
std::format("{:#o}", 8); // "010"
std::format("{:#b}", 6); // "0b110"
std::format("{:#.0f}", 1.0); // "1." (explicit decimal point)Zero-Fill (0)
Zero-padding is shorthand for fill='0' with align='>'. It respects the sign:
std::format("{:010d}", 42); // "0000000042"
std::format("{:010d}", -42); // "-000000042" (sign before zeros)
std::format("{:+010d}", 42); // "+000000042"
std::format("{:#010x}", 255); // "0x000000ff" (prefix counts toward width)Dynamic Width and Precision
Width and precision can be provided as runtime values using nested replacement fields:
int width = 10;
int prec = 3;
double val = 3.14159;
std::format("{:{}.{}f}", val, width, prec); // " 3.142"
std::format("{0:{1}.{2}f}", val, width, prec); // same, positionalThe nested argument must be a non-negative integer. This makes column-aligned table output straightforward:
// Print a table with runtime-determined column widths
void print_row(std::string_view name, double value, int col_width) {
std::println("{:<{}} = {:.4f}", name, col_width, value);
}Type Specifiers
Integers (int, long, unsigned, etc.):
| Type | Output |
|---|---|
d | Decimal (default) |
b | Binary |
B | Binary, uppercase prefix 0B with # |
o | Octal |
x | Hex lowercase |
X | Hex uppercase |
c | Character (Unicode code point) |
n | Locale-aware decimal (requires :Ln) |
Floats (float, double, long double):
| Type | Output |
|---|---|
f / F | Fixed-point (3.140000) |
e / E | Scientific notation (3.140000e+00) |
g / G | Shortest of f or e (removes trailing zeros) |
a / A | Hexadecimal float (0x1.91eb851eb851fp+1) |
| (none) | Same as g but always round-trips exactly |
Strings (std::string, std::string_view, const char*):
| Type | Output |
|---|---|
s | String (default) |
| (none) | Same as s |
Precision on a string type is a maximum width (truncates, never pads):
std::format("{:.5}", "hello world"); // "hello"
std::format("{:10.5}", "hello world"); // "hello " (pad to 10 after truncate)Runtime Formatting: std::vformat and std::make_format_args
When the format string is not a compile-time constant — for example in a logging framework that reads patterns from a config file — you need the type-erased runtime API.
#include <format>
// std::vformat accepts a runtime string_view and a type-erased arg list.
std::string result = std::vformat("{} + {} = {}", std::make_format_args(1, 2, 3));std::make_format_args creates a lightweight std::format_args object. The arguments are captured by reference, so they must outlive the call. This is intentional: it avoids allocations in the common case where vformat consumes the args immediately.
Building a Logging Framework
#include <format>
#include <string_view>
#include <source_location>
enum class Level { Debug, Info, Warn, Error };
class Logger {
Level min_level_;
std::string pattern_; // e.g. "[{level}] {msg}" — custom parsing not shown
public:
explicit Logger(Level min, std::string pattern)
: min_level_(min), pattern_(std::move(pattern)) {}
template <typename... Args>
void log(Level level, std::string_view fmt, Args&&... args) {
if (level < min_level_) return;
// Type-erased call: fmt may come from a config file or be user-supplied.
std::string msg = std::vformat(fmt, std::make_format_args(args...));
// Combine with metadata using a compile-time outer format.
std::println("[{}] {}", level_name(level), msg);
}
private:
static std::string_view level_name(Level l) {
switch (l) {
case Level::Debug: return "DEBUG";
case Level::Info: return "INFO";
case Level::Warn: return "WARN";
case Level::Error: return "ERROR";
}
return "?";
}
};
// Usage
Logger log(Level::Info, "");
log.log(Level::Info, "Connected to {}:{}", "db.local", 5432);
log.log(Level::Error, "Query failed: {}", err.message());std::vformat_to is the output-iterator variant, mirroring std::format_to:
std::string buf;
buf.reserve(256);
std::vformat_to(std::back_inserter(buf), fmt_str, std::make_format_args(a, b));C++23 Additions
std::print and std::println (P2093)
These write directly to a FILE* or std::ostream without constructing an intermediate std::string. On most platforms the output goes through a single OS write, making them faster than std::format followed by stream insertion.
#include <print>
// Write to stdout (default)
std::print("Hello, {}!\n", name);
std::println("Hello, {}!", name); // appends '\n' automatically
// Explicit target
std::println(std::cerr, "Fatal: code {}", exit_code);
std::println(stdout, "C FILE*: {}", value); // accepts FILE*std::print and std::println propagate std::format_error on bad format strings, just like std::format.
std::formatted_size
Returns the number of characters that std::format would produce, without actually forming the string. Useful for pre-allocating buffers:
#include <format>
std::size_t n = std::formatted_size("{:.4f}", 3.14159);
// n == 6 ("3.1416")
std::string s;
s.resize(n);
std::format_to(s.data(), "{:.4f}", 3.14159);std::format_to_n
Writes at most n characters to an output iterator and returns a struct with .out (the iterator) and .size (the total that would have been written, even if truncated):
#include <format>
char buf[16];
auto [out, total] = std::format_to_n(buf, sizeof(buf) - 1, "Value: {}", 123456789);
*out = '\0'; // null-terminate
// buf == "Value: 12345678" if truncated
// total == number of chars the full output would needThis is useful for fixed-size buffers (embedded systems, kernel code, protocol packetizers) where allocation is not acceptable.
Compile-Time Format String Validation (std::format_string)
std::format accepts its format argument as std::format_string<Args...>, a class template that performs compile-time validation. You can use it directly in your own functions to get the same safety guarantee:
#include <format>
// This function has the same compile-time checking as std::format itself.
template <typename... Args>
std::string my_format(std::format_string<Args...> fmt, Args&&... args) {
return std::format(fmt, std::forward<Args>(args)...);
}
// Compile error if the format string doesn't match the argument types:
// my_format("{:d}", "hello"); // error: 'd' not valid for stringstd::format_string is a consteval-constructed wrapper. The format string must be a constant expression — you cannot pass a std::string or char* variable; use std::vformat for runtime strings.
Performance: std::format vs printf vs ostringstream
std::format was designed with performance as a first-class requirement. The three main advantages over legacy APIs:
1. No locale overhead by default. printf and ostringstream consult the global locale for every numeric conversion. std::format is locale-independent unless you explicitly use the :L modifier. This eliminates mutex locking on locale objects in multi-threaded code.
2. Single allocation for the result. std::format measures the output size first (or grows a buffer geometrically), then returns a single std::string. ostringstream uses an internal std::stringbuf that copies into a second std::string on str().
3. Compile-time format string parsing. The format string is parsed once at compile time (for string literals). printf parses the format string at runtime on every call.
Benchmark data from Victor Zverovich's fmtlib paper (P0645) and subsequent implementations shows:
std::format/fmt::formatis 2–10x faster thanostringstreamfor typical integer and float formatting.std::formatis comparable to or faster thansprintffor integer types (no locale), and faster for floats because it uses the Ryu/Grisu3 algorithm for shortest-round-trip output instead of the Cdtoafamily.std::printis faster still because it skips the intermediatestd::stringentirely.
Practical guidance:
// Slow: two allocations, locale overhead
std::ostringstream oss;
oss << std::fixed << std::setprecision(2) << value;
std::string s = oss.str();
// Fast: one allocation, no locale
std::string s = std::format("{:.2f}", value);
// Fastest (C++23): no allocation for output
std::println("{:.2f}", value);For hot paths where even a single allocation matters, use std::format_to with a pre-allocated buffer:
std::string buf;
buf.reserve(64);
std::format_to(std::back_inserter(buf), "{:.2f}", value);
// buf holds the result; capacity retained for next call if reusedEscape Sequences and Literal Braces
Because { and } delimit replacement fields, they must be escaped when you want a literal brace in the output. Double the brace character:
| In format string | Output |
|---|---|
{{ | { |
}} | } |
Common examples:
std::format("{{}}"); // "{}"
std::format("{{{}}}", 42); // "{42}" — outer {{ and }} are literal braces
std::format("{{ value = {} }}", 99); // "{ value = 99 }"The "{{{}}}" example is worth unpacking step by step:
{{— escaped left brace, produces{{}— replacement field for the first argument (42)}}— escaped right brace, produces}- Result:
{42}
A lone { or } that is not part of a replacement field or escape sequence is a std::format_error at runtime (or a compile error for string literals):
// std::format("price: ${}}", 9.99); // error: unmatched '}' after the field
// std::format("{", 1); // error: unmatched '{'See Also
std::string_view— lightweight non-owning string reference (C++17)std::stringstream— older alternative; slower and more verbosefmtlibrary — predecessor tostd::format; backport available for older C++ standardssprintf,printf— C-style formatting; unsafe and locale-dependent