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

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++20

A 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 setlocale side effects; use :L to enable locale-aware formatting
  • Performance: Significantly faster than sprintf and ostringstream for typical formatting tasks
  • Familiar syntax: Similar to Python's str.format() and JavaScript template literals

Syntax

Basic Format String

cpp
#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 newline

Format Specification Mini-Language

cpp
{[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 (prefix 0x, 0b, 0o for integers; decimal point for floats)
  • 0: zero-pad (shorthand for fill='0' with align='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

cpp
// 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

cpp
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

cpp
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

cpp
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)

cpp
#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 allocation

Runtime Format Strings

cpp
// 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

cpp
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

  1. Prefer std::print over std::format + std::cout (C++23): Direct output is faster and avoids intermediate string allocation.

  2. Use compile-time format strings: String literals enable compile-time format checking. For runtime strings, use std::vformat and document why.

  3. Avoid locale-aware formatting unless needed: The default locale-independent formatting is faster and more predictable. Use :L only when explicitly required for user-facing output.

  4. For custom types, specialize std::formatter: More efficient and consistent than operator overloading with operator<<.

  5. Specify precision for floats explicitly: Default precision can vary; use .2f or .3g to be clear about output format.

Common Pitfalls

Compile-time checking requires string literal format:

cpp
std::format("{}", 42);                          // ✓ OK, compiles with check
std::string fmt = "{}";
std::vformat(fmt, std::make_format_args(42));   // ✓ OK, but no compile-time check

Format specifier mismatch is a compile error:

cpp
// std::format("{:d}", "hello");  // ✗ compile error: d is not valid for string

Boolean formatting defaults to 1/0 with {:d}:

cpp
std::format("{}", true);        // "true" (default)
std::format("{:d}", true);      // "1" (integer format)
std::format("{:b}", true);      // "true" (explicit boolean)

Precision truncates strings, not rounds:

cpp
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:

cpp
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

cpp
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:

cpp
#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

  • parse must be constexpr so format strings can be checked at compile time.
  • format must be const — the formatter object is shared across format calls.
  • Never throw from parse; throw std::format_error from format if a value is unrepresentable.
  • The partial specialization std::formatter<MyType> (no second parameter) is equivalent to std::formatter<MyType, char>. For wide-string support also specialize on wchar_t.

Format Spec Mini-Language: Complete Reference

The format specification sits between the : and } of a replacement field. Every field is optional:

cpp
{ [arg-id] [: [fill] [align] [sign] [#] [0] [width] [.precision] [type] ] }

Fill and Align

[fill][align] sets the padding character and direction. align is one of:

AlignMeaningDefault for
<Left-align, pad on rightstrings, booleans
>Right-align, pad on leftnumbers
^Center, equal padding both sides—

fill is any character that precedes the align character. If omitted, space is used.

cpp
std::format("{:*<10}", "hi");      // "hi********"
std::format("{:*>10}", "hi");      // "********hi"
std::format("{:*^10}", "hi");      // "****hi****"
std::format("{:_^15}", "center");  // "____center_____"

Sign

SpecMeaning
+Always show sign (+42, -42)
-Show sign only for negatives (default)
spaceSpace for positives, - for negatives ( 42, -42) — useful for aligning columns
cpp
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:

cpp
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:

cpp
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:

cpp
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, positional

The nested argument must be a non-negative integer. This makes column-aligned table output straightforward:

cpp
// 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.):

TypeOutput
dDecimal (default)
bBinary
BBinary, uppercase prefix 0B with #
oOctal
xHex lowercase
XHex uppercase
cCharacter (Unicode code point)
nLocale-aware decimal (requires :Ln)

Floats (float, double, long double):

TypeOutput
f / FFixed-point (3.140000)
e / EScientific notation (3.140000e+00)
g / GShortest of f or e (removes trailing zeros)
a / AHexadecimal float (0x1.91eb851eb851fp+1)
(none)Same as g but always round-trips exactly

Strings (std::string, std::string_view, const char*):

TypeOutput
sString (default)
(none)Same as s

Precision on a string type is a maximum width (truncates, never pads):

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

cpp
#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

cpp
#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:

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

cpp
#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:

cpp
#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):

cpp
#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 need

This 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:

cpp
#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 string

std::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::format is 2–10x faster than ostringstream for typical integer and float formatting.
  • std::format is comparable to or faster than sprintf for integer types (no locale), and faster for floats because it uses the Ryu/Grisu3 algorithm for shortest-round-trip output instead of the C dtoa family.
  • std::print is faster still because it skips the intermediate std::string entirely.

Practical guidance:

cpp
// 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:

cpp
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 reused

Escape 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 stringOutput
{{{
}}}

Common examples:

cpp
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):

cpp
// 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 verbose
  • fmt library — predecessor to std::format; backport available for older C++ standards
  • sprintf, printf — C-style formatting; unsafe and locale-dependent