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.
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