Skip to content
C++
Library
since C++20
Basic

std::format: Type-Safe Text Formatting in C++20

Learn to use C++20 std::format and std::print for type-safe, readable string formatting — replacing printf, sprintf, and stream concatenation with a clean, Python-style syntax.

By the end of this page you will use std::format to build strings with positional arguments, control alignment, width, precision, and base, write formatted output directly to a stream with std::print, and add formatting support to your own types.

What and Why

Before C++20, you had three choices for formatted text:

ApproachProblem
printf("%d", x)No type safety — wrong format specifier is undefined behaviour
std::cout << xVerbose; formatting state leaks between calls
Manual string concatenationCluttered, slow for many pieces

std::format (C++20, <format>) combines Python's {} placeholder syntax with C++'s type system. The format string is checked at compile time — a mismatch between {} specifiers and argument types is a hard error, not a runtime crash.

cpp
#include <format>
#include <iostream>

int main() {
    std::string s = std::format("Hello, {}! You are {} years old.", "Alice", 30);
    std::cout << s << '\n';
    // Hello, Alice! You are 30 years old.
}

Step by Step

Basic substitution

Every {} in the format string is a replacement field that consumes the next argument:

cpp
#include <format>
#include <string>

std::string result = std::format("{} + {} = {}", 1, 2, 3);
// "1 + 2 = 3"

You can also specify argument positions explicitly — useful when the same value appears more than once or when you need to reorder:

cpp
std::string s = std::format("{0} is {1} and {0} is also {2}", "Alice", "smart", "kind");
// "Alice is smart and Alice is also kind"
//  ^0           ^1         ^0             ^2

Width, fill, and alignment

The format spec goes after a colon inside {}. Width sets the minimum field size; fill and alignment control padding:

cpp
// {:<width>}  — minimum width, default alignment
std::format("{:10}",  42)      // "        42"   (right-aligned for numbers)
std::format("{:10}",  "hi")    // "hi        "   (left-aligned for strings)

// Explicit alignment: < left, > right, ^ center
std::format("{:<10}", 42)      // "42        "
std::format("{:>10}", "hi")    // "        hi"
std::format("{:^10}", "hi")    // "    hi    "

// Fill character before the alignment option
std::format("{:*^10}", "hi")   // "****hi****"
std::format("{:0>6}", 42)      // "000042"

Numeric formatting

cpp
// Integers
std::format("{:d}", 255)       // "255"      decimal (default)
std::format("{:b}", 255)       // "11111111" binary
std::format("{:o}", 255)       // "377"      octal
std::format("{:x}", 255)       // "ff"       hex lowercase
std::format("{:X}", 255)       // "FF"       hex uppercase
std::format("{:#x}", 255)      // "0xff"     hex with prefix
std::format("{:+d}", 42)       // "+42"      always show sign
std::format("{:08x}", 255)     // "000000ff" zero-padded to width 8

// Floating-point
std::format("{:.2f}", 3.14159) // "3.14"     fixed, 2 decimal places
std::format("{:.4e}", 3.14159) // "3.1416e+00" scientific
std::format("{:g}", 3.14)      // "3.14"     shortest representation
std::format("{:10.3f}", 3.14)  // "     3.140" width 10, precision 3

Dynamic width and precision

Width and precision can themselves be arguments, passed as a nested {}:

cpp
int width = 10;
int prec  = 3;
float pi  = 3.14159f;

std::format("{:{}.{}f}", pi, width, prec)  // "     3.142"
//                         ^     ^
//                       width  precision from arguments

This is the std::format equivalent of printf("%*.*f", width, prec, pi) but without the type hazard.

std::format_to — write to an output iterator

When you want to append formatted text to an existing buffer without building a temporary std::string:

cpp
#include <format>
#include <vector>

std::vector<char> buf;
std::format_to(std::back_inserter(buf), "x = {}, y = {}", 3, 4);
// buf now contains: 'x', ' ', '=', ' ', '3', ',', ' ', 'y', ' ', '=', ' ', '4'

std::format_to_n adds a size cap — useful for fixed-size char arrays:

cpp
char buf[64];
auto result = std::format_to_n(buf, sizeof(buf) - 1, "value = {}", 42);
*result.out = '\0';   // null-terminate

std::print and std::println (C++23)

C++23 adds std::print and std::println to <print>, combining formatting and output in one call:

cpp
#include <print>

std::print("Hello, {}!\n", "world");       // no newline in format string
std::println("Hello, {}!", "world");       // appends '\n' automatically
std::println(stderr, "Error: {}", msg);    // write to stderr

std::println is now the idiomatic replacement for std::cout << combined with std::format.

Common Patterns

Pattern 1 — table-like output with column alignment

cpp
#include <format>
#include <iostream>
#include <vector>

struct Row { std::string name; int score; double ratio; };

int main() {
    std::vector<Row> rows = {{"Alice", 95, 0.95}, {"Bob", 72, 0.72}, {"Carol", 88, 0.88}};

    std::cout << std::format("{:<10} {:>6} {:>8}\n", "Name", "Score", "Ratio");
    std::cout << std::string(26, '-') << '\n';
    for (const auto& r : rows)
        std::cout << std::format("{:<10} {:>6} {:>8.2f}\n", r.name, r.score, r.ratio);
}
// Name        Score    Ratio
// --------------------------
// Alice          95     0.95
// Bob            72     0.72
// Carol          88     0.88

Pattern 2 — hex dump

cpp
#include <format>
#include <span>
#include <string>

std::string hex_dump(std::span<const std::byte> data) {
    std::string out;
    for (std::size_t i = 0; i < data.size(); ++i) {
        if (i && i % 16 == 0) out += '\n';
        std::format_to(std::back_inserter(out),
                       "{:02x} ", static_cast<unsigned>(data[i]));
    }
    return out;
}

Pattern 3 — formatting your own type

Specialize std::formatter<T> to make T formattable:

cpp
#include <format>

struct Point { double x, y; };

template <>
struct std::formatter<Point> {
    // parse() receives the spec between ':' and '}' — we ignore it here
    constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }

    auto format(const Point& p, std::format_context& ctx) const {
        return std::format_to(ctx.out(), "({:.2f}, {:.2f})", p.x, p.y);
    }
};

int main() {
    Point p{3.0, 4.0};
    std::println("point = {}", p);   // point = (3.00, 4.00)
}

Once formatter<Point> exists, Point works inside any nested format string, including inside containers formatted with std::format.

What Can Go Wrong

Wrong number of arguments

cpp
std::format("{} {}", 42);   // compile error: too few arguments
std::format("{}", 1, 2);    // compile error: too many arguments

The format string is validated at compile time via std::format_string<Args...>. These errors appear at the call site, not inside <format>.

Invalid specifier for the type

cpp
std::format("{:d}", 3.14);  // compile error: 'd' is not valid for double
std::format("{:.2f}", "hi"); // compile error: precision not valid for strings

Again caught at compile time.

Runtime format strings

When the format string is not a compile-time constant — for example, loaded from a config file — you cannot pass it directly. Use std::runtime_format (C++26) or std::vformat:

cpp
// C++20/23 — runtime format string
std::string fmt = load_fmt_from_config();
std::string result = std::vformat(fmt, std::make_format_args(42, "hello"));

// C++26 — cleaner wrapper
std::string result2 = std::format(std::runtime_format(fmt), 42, "hello");

std::vformat cannot check the format string at compile time. A mismatch throws std::format_error at runtime.

std::format vs std::to_string

std::to_string converts a single number to string with no formatting control. Use std::format whenever you need width, precision, base, alignment, or you're combining multiple values:

cpp
// std::to_string — single value, no control
std::to_string(3.14)          // "3.140000" (6 decimal places, always)

// std::format — explicit precision
std::format("{:.2f}", 3.14)   // "3.14"

Quick Reference

GoalSyntax
Basic substitutionstd::format("{}", value)
Positional argstd::format("{0} {1} {0}", a, b)
Minimum width{:10}
Left / right / center align{:<10} / {:>10} / {:^10}
Fill character{:*^10}
Zero-pad{:08}
Decimal / hex / binary / octal{:d} / {:x} / {:b} / {:o}
Hex with prefix{:#x}
Always show sign{:+}
Fixed float, 2 dp{:.2f}
Scientific{:.3e}
Dynamic width/precision{:{}.{}f}
Locale-specific separators{:L}
Debug/escaped string (C++23){:?}
Write to bufferstd::format_to(out_iter, ...)
Write to stdoutstd::print(...) (C++23)
Write with newlinestd::println(...) (C++23)

Header: <format> for std::format/format_to; <print> for std::print/println (C++23).

Custom Formatters — std::formatter<MyType, char>

Any user-defined type can be made formattable by specializing std::formatter. The specialization requires two member functions: parse, which reads the format spec between : and }, and format, which writes the formatted representation to the output iterator stored in the format context.

cpp
#include <format>
#include <string_view>

struct Color { uint8_t r, g, b; };

template <>
struct std::formatter<Color> {
    // Presentation mode: 'h' for hex (#rrggbb), 'd' for decimal (default)
    char mode = 'd';

    constexpr auto parse(std::format_parse_context& ctx) {
        auto it = ctx.begin();
        if (it != ctx.end() && *it == 'h') { mode = 'h'; ++it; }
        if (it != ctx.end() && *it != '}')
            throw std::format_error("invalid Color format spec");
        return it;
    }

    auto format(const Color& c, std::format_context& ctx) const {
        if (mode == 'h')
            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);
    }
};

int main() {
    Color red{255, 0, 0};
    std::println("{}", red);    // rgb(255,0,0)
    std::println("{:h}", red);  // #FF0000
}

The parse function must advance the iterator past every character it consumes and return it pointing at the closing }. Throwing std::format_error from parse produces a compile-time error when the format string is a constant.

A common shortcut is to delegate to an existing formatter. If your type has a natural std::string representation, inherit from std::formatter<std::string> and call its format after converting:

cpp
template <>
struct std::formatter<Color> : std::formatter<std::string> {
    auto format(const Color& c, std::format_context& ctx) const {
        return std::formatter<std::string>::format(
            std::format("rgb({},{},{})", c.r, c.g, c.b), ctx);
    }
};

This approach inherits the full string format spec (width, fill, alignment) for free.

Compile-Time Format String Validation

std::format validates its format string against the argument types at compile time — a mismatched specifier is a hard error, not a runtime crash. This guarantee comes from the std::format_string<Args...> wrapper type that the format string is implicitly converted to when calling std::format.

cpp
// All of these are compile errors, caught at the call site:
std::format("{:d}", 3.14);       // 'd' not valid for double
std::format("{:.2f}", "text");   // precision not valid for strings
std::format("{} {}", 1);         // too few arguments
std::format("{}", 1, 2, 3);      // too many arguments
std::format("{:z}", 42);         // unknown specifier 'z'

The mechanism works because std::format_string<Args...> has a consteval constructor that evaluates the format string parsing at compile time. You get the error message at the std::format(...) call, not buried inside <format> internals.

When you need to store a format string in a variable and pass it at runtime, you must opt out of static checking:

cpp
// Runtime format string — no static check, throws std::format_error on mismatch
std::string fmt = "value = {:d}";
std::string result = std::vformat(fmt, std::make_format_args(42));

// C++26: std::runtime_format makes the intent explicit
std::string result2 = std::format(std::runtime_format(fmt), 42);

std::vformat is the underlying variadic overload that accepts std::format_args instead of a typed pack. It is the right tool for logging frameworks and config-driven output where the format string is not known until runtime.

std::format_to and Output Iterators

std::format_to writes directly to any output iterator, avoiding the temporary std::string that std::format must allocate. This makes it the preferred choice when appending to an existing buffer, writing to a fixed-size array, or building output incrementally.

cpp
#include <format>
#include <string>
#include <vector>

// Append to std::string
std::string log;
log.reserve(256);
std::format_to(std::back_inserter(log), "[{}] {}\n", "INFO", "server started");
std::format_to(std::back_inserter(log), "[{}] {}\n", "WARN", "low memory");

// Append to std::vector<char>
std::vector<char> buf;
std::format_to(std::back_inserter(buf), "x={}, y={}", 3, 4);

// Write directly into a pre-allocated string (using string::iterator)
std::string fixed(64, '\0');
auto end = std::format_to(fixed.begin(), "pi = {:.5f}", 3.14159265);
fixed.resize(std::distance(fixed.begin(), end));

std::format_to_n adds a size cap, which is essential for fixed char arrays and protocol buffers:

cpp
char packet[32];
auto [out, count] = std::format_to_n(packet, sizeof(packet) - 1,
                                      "CMD:{} SEQ:{:04d}", "PING", 42);
*out = '\0';   // null-terminate
// count is the total characters that *would* have been written,
// even if truncated — use it to detect truncation
if (count >= sizeof(packet))
    throw std::runtime_error("packet truncated");

The return type of format_to_n is std::format_to_n_result<Out> with members out (iterator past last written character) and count (number of characters that would have been written without truncation).

Locale-Aware Formatting

The :L flag instructs std::format to use the thread's locale for number grouping, decimal separators, and boolean names. Without :L, formatting is always locale-independent (dot as decimal, no digit separators).

cpp
#include <format>
#include <locale>

std::locale::global(std::locale("en_US.UTF-8"));

std::format("{:L}", 1'234'567)       // "1,234,567"  — US locale groups with comma
std::format("{:.2Lf}", 3.14159)      // "3.14"        — decimal point (en_US)
std::format("{:Ld}", true)           // "true"         — localized boolean

std::locale::global(std::locale("de_DE.UTF-8"));

std::format("{:L}", 1'234'567)       // "1.234.567"  — German locale
std::format("{:.2Lf}", 3.14159)      // "3,14"        — comma as decimal separator

You can also pass a locale object explicitly to std::format via the overload that accepts std::locale:

cpp
auto de = std::locale("de_DE.UTF-8");
std::string s = std::format(de, "{:L}", 9876543.21);  // "9.876.543,21"

The :L flag works for integers, floating-point, and booleans. Strings and characters are unaffected by locale in std::format.

Width, Fill, Align, and Sign — Full Reference

The full format spec grammar fits a consistent pattern: [[fill]align][sign][#][0][width][.precision][L][type]. Understanding each slot lets you build any numeric or string presentation without trial and error.

cpp
// Fill + align
std::format("{:*<10}", "hi")    // "hi********"   fill='*', align=left
std::format("{:*>10}", "hi")    // "********hi"   fill='*', align=right
std::format("{:*^10}", "hi")    // "****hi****"   fill='*', align=center
std::format("{:010}", 42)       // "0000000042"   zero-fill (numbers only)

// Sign
std::format("{:+}", 42)         // "+42"    always show sign
std::format("{:+}", -42)        // "-42"
std::format("{: }", 42)         // " 42"    space for positive (aligns with negative)
std::format("{:-}", 42)         // "42"     only show minus (default)

// Alternate form (#)
std::format("{:#b}", 6)         // "0b110"  binary with prefix
std::format("{:#o}", 8)         // "010"    octal with prefix
std::format("{:#x}", 255)       // "0xff"   hex with prefix
std::format("{:#X}", 255)       // "0XFF"
std::format("{:#.3f}", 1.0)     // "1.000"  always show decimal point and trailing zeros

// Precision (strings — max characters; floats — decimal digits)
std::format("{:.5}", "hello world")   // "hello"  truncate string to 5 chars
std::format("{:.3f}", 3.14159)        // "3.142"  3 decimal places
std::format("{:.3g}", 0.000123)       // "0.000123" → "0.000123" (g picks fixed/sci)
std::format("{:.3e}", 12345.6789)     // "1.235e+04"

The type code controls the presentation base. For integers: d (decimal), b (binary), o (octal), x/X (hex). For floats: f (fixed), e/E (scientific), g/G (shortest), a/A (hex float). For strings: s (default) or ? (escaped, C++23).

std::print and std::println (C++23)

std::print and std::println are C++23 additions that combine std::format with output in a single call, replacing the std::cout << std::format(...) idiom. They write directly to a FILE* (defaulting to stdout) using efficient buffered I/O that avoids the overhead of std::ostream.

cpp
#include <print>   // C++23

// Basic usage
std::print("Hello, {}!\n", "world");        // stdout, no automatic newline
std::println("Hello, {}!", "world");        // stdout, appends '\n'

// Explicit stream
std::println(stderr, "Error: {} (code {})", msg, errno);
std::println(stdout, "Written {} bytes", n);

// Works with all format features
std::println("{:>20}", "right-aligned");
std::println("{:#010x}", 0xDEAD);           // "0x0000dead"
std::println("{:.2f}", 3.14159);            // "3.14"

std::print is more efficient than std::cout << std::format(...) because it writes the formatted output directly to the underlying file descriptor in one shot, without going through the std::ostream synchronization machinery. When performance matters for logging or diagnostics, prefer std::print over the combination.

One important note: std::print and std::println flush the buffer on '\n' when writing to a terminal (the same way std::cout does in line-buffered mode), so there is no need to add explicit std::flush calls for interactive output.

Migrating from printf to std::format

std::format has a direct equivalent for every common printf conversion specifier — the main differences are the syntax ({} vs %) and the fact that std::format never relies on implicit type coercion.

printf specifierstd::format equivalentNotes
%d{} or {:d}int — {} deduces type automatically
%u{}unsigned int
%ld{}long — no length modifier needed
%lld{}long long
%f{:f}fixed-point float
%e{:e}scientific notation
%g{:g}shortest float representation
%.2f{:.2f}2 decimal places
%10.3f{:10.3f}width 10, 3 decimal places
%s{}string — null terminator not required
%c{}char
%x{:x}lowercase hex
%X{:X}uppercase hex
%#x{:#x}hex with 0x prefix
%08x{:08x}zero-padded hex
%-10s{:<10}left-aligned, width 10
%+d{:+}always show sign
%*d{:{}}dynamic width from argument
%pno direct equivalentcast pointer to uintptr_t and use {:#x}
cpp
// Before: printf
printf("%-10s %5d %8.2f\n", name, score, ratio);

// After: std::format
std::println("{:<10} {:>5} {:>8.2f}", name, score, ratio);

The key ergonomic improvements: argument order is enforced by the type system, there are no silent truncations from mismatched lengths (%d for a long), and the format string is checked at compile time. The only printf capability that has no std::format counterpart is %n (writing the character count into an integer pointer) — which is a security vulnerability anyway and disabled on most modern platforms.

What's Next

  • std::string_view — zero-copy string references, commonly paired with format output.
  • Strings — the std::string fundamentals that std::format builds on.
  • format reference — the full format specification grammar and all type codes.