std::format
Type-safe, compile-time-checked text formatting introduced in C++20, replacing printf and stream-based output with Python-style format strings.
std::formatsince C++20A type-safe, extensible string formatting function from <format> that accepts a compile-time format string with {} replacement fields and returns a std::string.
Overview
std::format models Python's str.format() with compile-time type guarantees. Unlike printf, type mismatches are caught at build time. Unlike std::ostringstream, formatting logic is decoupled from the value — custom types are formatted via a specialization point, not operator<<.
The header is <format>. C++23 adds <print> for std::print and std::println, and extends range formatting (std::vector, std::tuple, etc.) directly through <format>.
C++20 vs. C++23 enforcement: C++20 compilers required the format string to be a constant expression by convention; enforcement was implementation-defined. C++23 (P2216) formally mandates it — passing a runtime std::string as the first argument is now a compile error. Runtime format strings require std::vformat.
Syntax
// <format> — C++20
std::string std::format(format-string fmt, Args&&... args);
void std::format_to(OutputIt out, format-string fmt, Args&&... args);
std::size_t std::formatted_size(format-string fmt, Args&&... args);
auto std::format_to_n(OutputIt out, std::size_t n, format-string fmt, Args&&... args);
// Runtime format string — C++20
std::string std::vformat(std::string_view fmt, std::format_args args);
void std::vformat_to(OutputIt out, std::string_view fmt, std::format_args args);
// <print> — C++23
void std::print(format-string fmt, Args&&... args); // stdout
void std::println(format-string fmt, Args&&... args); // stdout + '\n'
void std::print(std::FILE* f, format-string fmt, ...); // FILE* overload
void std::print(std::ostream& os, format-string fmt, ...); // ostream overloadExamples
Format specifier mini-language
The spec syntax inside braces is {[index]:[fill][align][sign][#][0][width][.precision][L][type]}.
#include <format>
// Integers
std::format("{:d}", 255); // "255" decimal
std::format("{:x}", 255); // "ff" lowercase hex
std::format("{:#x}", 255); // "0xff" alternate form adds prefix
std::format("{:08x}", 255); // "000000ff" zero-padded to width 8
std::format("{:b}", 255); // "11111111" binary
std::format("{:#010b}", 3); // "0b00000011"
// Floating point
std::format("{:.4f}", 3.14159265); // "3.1416" fixed, 4 decimal places
std::format("{:.4e}", 3.14159265); // "3.1416e+00" scientific
std::format("{:.4g}", 3.14159265); // "3.142" shortest representation
std::format("{:+.2f}", 3.14); // "+3.14" force sign
// Alignment and fill
std::format("{:>10}", "hi"); // " hi" right (number default)
std::format("{:<10}", 42); // "42 " left
std::format("{:^10}", 42); // " 42 " centered
std::format("{:*^10}", 42); // "****42****" custom fill char
// Strings with precision — truncation, not rounding
std::format("{:.5}", "truncate"); // "trunc"
// Boolean
std::format("{}", true); // "true" (default: alpha)
std::format("{:d}", true); // "1" integer representationPositional indices and dynamic arguments
// Reuse an argument multiple times
std::format("{0}, {1}, {0}", "ping", "pong"); // "ping, pong, ping"
// Dynamic width and precision — referenced argument must be integral
int width = 12, prec = 3;
std::format("{:{}.{}f}", 3.14159, width, prec); // " 3.142"
// Locale-aware formatting — C++20; {:L} uses the thread locale
std::format(std::locale("en_US.UTF-8"), "{:L}", 1'000'000); // "1,000,000"
std::format(std::locale("de_DE.UTF-8"), "{:L}", 1'000'000); // "1.000.000"
// Avoid {:L} for machine-readable output (logs, serialisation)Output without heap allocation
#include <format>
#include <array>
// format_to — write into any OutputIterator; no intermediate std::string
std::string buf;
buf.reserve(64);
std::format_to(std::back_inserter(buf), "({:.2f}, {:.2f})", 1.0, 2.0);
// format_to_n — bounded write; returns {out_iterator, total_size}
std::array<char, 32> fixed;
auto [end, total] = std::format_to_n(fixed.data(), fixed.size() - 1, "{:.6f}", 3.14);
*end = '\0'; // null-terminate manually
// formatted_size — determine required buffer without writing
std::size_t needed = std::formatted_size("{:08.3f}", 42.7); // 8
// C++23: direct output — no std::cout, no manual '\n' on println
std::print(std::cerr, "[error] {}: {}\n", errno, strerror(errno));
std::println("Finished in {:.2f}s", elapsed);Custom type formatting
Specialize std::formatter<T>. parse receives a format_parse_context pointing just after the : in the spec; advance and return the iterator. format writes to ctx.out().
#include <format>
struct Rgb { uint8_t r, g, b; };
template <>
struct std::formatter<Rgb> {
bool hex = false;
constexpr auto parse(std::format_parse_context& ctx) {
auto it = ctx.begin();
if (it != ctx.end() && *it == 'x') {
hex = true;
++it;
}
if (it != ctx.end() && *it != '}')
throw std::format_error("invalid Rgb format spec");
return it;
}
auto format(const Rgb& c, std::format_context& ctx) const {
if (hex)
return std::format_to(ctx.out(), "#{:02X}{:02X}{:02X}", c.r, c.g, c.b);
return std::format_to(ctx.out(), "rgb({}, {}, {})", c.r, c.g, c.b);
}
};
std::format("{}", Rgb{255, 0, 0}); // "rgb(255, 0, 0)"
std::format("{:x}", Rgb{255, 0, 0}); // "#FF0000"For wrapper types, inherit from an existing formatter to get alignment and width for free:
template <>
struct std::formatter<MyPath> : std::formatter<std::string> {
auto format(const MyPath& p, std::format_context& ctx) const {
return std::formatter<std::string>::format(p.native(), ctx);
}
// parse() is inherited — all string specs (width, align, truncation) work
};Runtime format strings
When the format string is not a compile-time constant (logging frameworks, config-driven output), use std::vformat:
#include <format>
void emit(std::string_view level, std::string_view fmt, std::format_args args) {
std::string msg = std::vformat(fmt, args);
write_log(level, msg);
}
// Caller:
emit("INFO", "{} connections, {:.1f}% capacity",
std::make_format_args(conn_count, pct));std::make_format_args captures references — the resulting std::format_args is valid only for the lifetime of its arguments.
Range formatting (C++23)
#include <format>
#include <vector>
#include <map>
std::vector<int> v{1, 2, 3};
std::format("{}", v); // "[1, 2, 3]"
std::format("{:n}", v); // "1, 2, 3" no brackets
std::format("{::d}", v); // "[1, 2, 3]" per-element spec after '::'
std::format("{::08b}", v); // "[00000001, 00000010, 00000011]"
std::map<std::string, int> m{{"b", 2}, {"a", 1}};
std::format("{}", m); // "{'a': 1, 'b': 2}"Best Practices
Use format_to on pre-reserved buffers for hot paths. std::format always allocates. std::format_to with a std::string after reserve(), or a stack std::array<char, N>, avoids extra allocation. Call std::formatted_size first when the output size varies.
Inherit from existing formatters for wrapper types. Rather than re-implementing fill, align, and width logic, inherit from std::formatter<std::string> or std::formatter<double> and delegate parse() to the base.
Gate {:L} to user-visible output. Locale-aware formatting inserts locale-correct separators and decimal points. Never use it for serialised data, log files, or protocol output where a specific format is required.
Prefer std::println over std::cout in C++23. std::println doesn't synchronise with C stdio by default, has no state to corrupt (unlike std::ios flags), and composes naturally with std::format's compile-time checking.
Common Pitfalls
Format string must be a compile-time constant (C++23)
std::string fmt = "{} {}";
auto s = std::format(fmt, a, b); // compile error in C++23
// Fix: use vformat for runtime strings
auto s = std::vformat(fmt, std::make_format_args(a, b));std::format_args holds references, not copies
// UB: format_args outlives the arguments it references
std::format_args bad() {
int x = 42;
return std::make_format_args(x); // x is destroyed on return
}Construct std::format_args and call vformat in the same expression or scope.
std::vformat throws on bad specs — std::format does not
std::format diagnoses type/spec mismatches at compile time. With std::vformat, a malformed or type-incompatible format string throws std::format_error at runtime. Wrap accordingly in any code that takes user-supplied format strings:
try {
result = std::vformat(user_fmt, std::make_format_args(value));
} catch (const std::format_error& e) {
// reject or sanitise
}No named arguments
The standard does not support {name} syntax. Use positional indices {0}, {1} for argument reuse, or define a struct with a custom std::formatter specialization if labelled output is required.
Custom Type Formatting with std::formatter
Specializing std::formatter<T> is the standard extension point for user-defined types. parse() receives the format spec (everything after : up to }), advances the iterator over any specifiers it recognises, and returns the iterator at }. format() writes the representation to ctx.out().
// C++20
#include <format>
struct Point { double x, y; };
template <>
struct std::formatter<Point> {
int width = 0;
int precision = 6;
char fill = ' ';
char align = '>';
constexpr auto parse(std::format_parse_context& ctx) {
auto it = ctx.begin();
// Optional fill+align
if (it != ctx.end() && *(it + 1) == '<') { fill = *it++; align = '<'; ++it; }
else if (it != ctx.end() && *(it + 1) == '>') { fill = *it++; align = '>'; ++it; }
else if (it != ctx.end() && *(it + 1) == '^') { fill = *it++; align = '^'; ++it; }
// Optional width
while (it != ctx.end() && *it >= '0' && *it <= '9')
width = width * 10 + (*it++ - '0');
// Optional precision
if (it != ctx.end() && *it == '.') {
++it;
precision = 0;
while (it != ctx.end() && *it >= '0' && *it <= '9')
precision = precision * 10 + (*it++ - '0');
}
if (it != ctx.end() && *it != '}')
throw std::format_error("invalid Point format spec");
return it;
}
auto format(const Point& p, std::format_context& ctx) const {
// Build the inner representation using the parsed precision
std::string inner = std::format("({:.{}f}, {:.{}f})",
p.x, precision, p.y, precision);
// Apply width/fill/align via a string formatter
std::formatter<std::string> sf;
// Delegate to std::formatter<std::string> for alignment
return std::format_to(ctx.out(), "{:{}{}{}}",
inner, fill, align, width);
}
};
Point pt{1.5, 2.75};
std::format("{}", pt); // "(1.500000, 2.750000)"
std::format("{:.2f}", pt); // "(1.50, 2.75)" -- precision via parse()
std::format("{:>12.2f}", pt); // " (1.50, 2.75)" -- width+align appliedformat_parse_context exposes begin() / end() iterators over the spec string, and check_arg_id() for validating positional references in nested specs. format_context provides out() (the output iterator) and arg(n) for accessing individual arguments by index.
std::format_to and std::format_to_n
std::format_to writes directly into any OutputIterator, avoiding the intermediate std::string allocation that std::format always performs. This matters in hot paths such as log serialisation or packet assembly, where every allocation counts.
// C++20
#include <format>
#include <string>
#include <array>
#include <iterator>
// Append to an existing string (no temporary)
std::string log_line;
log_line.reserve(128);
std::format_to(std::back_inserter(log_line), "[{}] {}: {}",
timestamp, level, message);
// Format into a pre-allocated heap buffer
std::string buf(64, '\0');
auto out_it = std::format_to(buf.begin(), "x={:.4f}", 3.14159);
buf.resize(std::distance(buf.begin(), out_it)); // trim to actual length
// Bounded write into a stack buffer — safe when size is not known ahead of time
std::array<char, 64> fixed{};
auto [end, total] = std::format_to_n(fixed.data(), fixed.size() - 1,
"pid={} tid={}", pid, tid);
*end = '\0'; // null-terminate; total >= fixed.size()-1 means truncation occurred
// formatted_size — query required capacity without writing
std::size_t needed = std::formatted_size("0x{:016X}", address);
std::string s(needed, '\0');
std::format_to(s.begin(), "0x{:016X}", address);std::format_to_n returns a struct {out, size} where out is the iterator past the last character written and size is the total number of characters that would have been written without the bound — use size >= n to detect truncation without a separate formatted_size call.
std::print and std::println (C++23)
std::print and std::println are the C++23 terminal-output counterparts to std::format, writing directly to a FILE* or std::ostream without constructing an intermediate std::string. On Windows they call WriteConsoleW when the destination is a console handle, ensuring correct Unicode rendering for characters outside the active code page.
// C++23
#include <print>
std::string name = "world";
std::print("Hello, {}!\n", name); // stdout, no extra allocation
double value = 3.14159;
std::println("{:.4f}", value); // stdout + automatic '\n'
std::print(stderr, "Error: {}\n", msg); // write to C FILE* stream
std::println(std::cerr, "Error: {}", msg); // write to std::ostream
// Compare: the cout equivalent allocates a temporary std::string
std::cout << std::format("value = {}\n", value); // extra allocation
std::println("value = {}", value); // no allocationstd::println is strictly equivalent to std::print followed by '\n' — it does not flush the stream. To flush, call std::fflush(stdout) or use std::cout with std::flush. Both functions inherit all std::format specifiers; the first argument may be a FILE* or std::ostream& before the format string.
Dynamic Format Arguments with std::vformat
std::vformat accepts a std::string_view format string resolved at runtime, enabling patterns like localisation files and user-configurable output templates. The trade-off is that format errors become std::format_error exceptions rather than compile-time diagnostics.
// C++20
#include <format>
#include <stdexcept>
// Load a format template from an external source (config, gettext catalogue…)
std::string load_template(const std::string& key); // returns e.g. "{} items at ${:.2f} each"
void print_invoice(int qty, double price) {
std::string tmpl = load_template("invoice_line");
try {
std::string line = std::vformat(tmpl, std::make_format_args(qty, price));
std::puts(line.c_str());
} catch (const std::format_error& e) {
// Malformed or type-incompatible template from external source
std::puts(e.what());
}
}
// Compile-time wrapper — std::format_string<Args...> performs
// the same parse-time check that std::format enforces:
template <typename... Args>
std::string safe_format(std::format_string<Args...> fmt, Args&&... args) {
return std::format(fmt, std::forward<Args>(args)...); // compile error on bad spec
}
// Contrast: vformat accepts runtime string_view (no compile check)
std::string rt_fmt = "{} {}";
std::vformat(rt_fmt, std::make_format_args(1, 2)); // fine, checked at runtime
// std::format(rt_fmt, 1, 2); // compile error in C++23std::make_format_args captures its arguments by reference, so the returned std::format_args must not outlive them. For wchar_t-based output use std::vformat from <format> with the std::wformat_args / std::make_wformat_args variants.
std::format vs printf vs iostream Performance
std::format and the {fmt} library it is based on achieve throughput comparable to printf for most workloads while adding compile-time type safety and locale-independence. std::ostringstream is the slowest option because each insertion goes through locale facets, even when locale-independent output is intended.
// C++20 — illustrative benchmark loop (numbers vary by platform/compiler)
#include <format>
#include <cstdio>
#include <sstream>
// std::format — one heap allocation per call, no locale overhead
std::string s1 = std::format("{:.6f}", 3.14159265);
// printf — no allocation, no type safety, undefined behaviour on mismatch
char buf[32];
std::snprintf(buf, sizeof(buf), "%.6f", 3.14159265);
// std::cout — synchronized with C stdio by default; locale facets on every <<
std::cout << std::fixed << std::setprecision(6) << 3.14159265 << '\n';
// std::ostringstream — locale facets + stream state overhead
std::ostringstream oss;
oss << std::fixed << std::setprecision(6) << 3.14159265;
std::string s2 = oss.str();| Approach | Type-safe | Compile-time check | Locale-independent | Allocations | Relative throughput |
|---|---|---|---|---|---|
std::format | Yes | Yes (C++23) | Yes (default) | 1 per call | ~High |
fmt::format | Yes | Yes | Yes (default) | 1 per call | ~High |
printf / snprintf | No | No | Yes | 0 (buffer) | Highest (simple) |
std::cout | Yes | No | No (sync + locale) | 0 | Low |
std::ostringstream | Yes | No | No (locale) | 1+ per call | Lowest |
Use std::format_to into a pre-reserved buffer to match snprintf performance while retaining type safety. Avoid std::ostringstream in hot paths; prefer std::format_to with a std::string after reserve().
Formatting Ranges and Containers (C++23)
C++23 adds built-in std::formatter specializations for any type that models std::ranges::range, so std::vector, std::list, std::set, std::map, std::tuple, and std::pair are all directly formattable without any boilerplate. The format spec is divided by : into a range spec (brackets, separator) and a nested element spec.
// C++23
#include <format>
#include <vector>
#include <map>
#include <tuple>
std::vector<int> vec{1, 2, 3};
std::format("{}", vec); // "[1, 2, 3]" default: brackets + ", "
std::format("{:n}", vec); // "1, 2, 3" :n suppresses brackets
std::format("{::02x}", vec); // "[01, 02, 03]" :: introduces per-element spec
std::vector<double> fvec{1.5, 2.25, 3.125};
std::format("{::.2f}", fvec); // "[1.50, 2.25, 3.12]"
std::map<std::string, int> m{{"b", 2}, {"a", 1}};
std::format("{}", m); // "{'a': 1, 'b': 2}" pairs use (key, value) form
std::tuple<int, std::string, double> t{42, "hello", 1.5};
std::format("{}", t); // "(42, hello, 1.5)"
// std::string, std::string_view, and std::filesystem::path format correctly
// as strings (not as character ranges):
std::filesystem::path p{"/usr/local/bin"};
std::format("{}", p); // "/usr/local/bin" (quoted on POSIX if needed)
// Custom range formatting via std::range_formatter<T>
template <typename T>
struct std::formatter<MyContainer<T>> : std::range_formatter<T> {
constexpr MyContainer_formatter() {
this->set_brackets("[[[", "]]]"); // override default '[' ']'
this->set_separator(" | "); // override default ", "
}
};The nested spec after :: applies to each element individually, so {::08b} formats every integer in a vector as an 8-digit binary number. std::string and std::string_view are excluded from range formatting despite being ranges — they format as strings, not as [h, e, l, l, o].
See Also
std::print,std::println— C++23 output built directly onstd::formatstd::formatter— customization point for user-defined type formattingstd::vformat,std::make_format_args— runtime format string support<chrono>formatting —std::chrono::time_point, duration, and calendar types have built-instd::formatterspecializations since C++20std::to_string— single-value conversion with no format control