Skip to content
C++
Language
since C++23
Basic

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

Type-safe, Unicode-aware formatted output using std::format syntax. Direct FILE* and ostream writes with compile-time format string validation.

std::print / std::printlnsince C++23

Type-safe output functions that accept a std::format-syntax format string and write the result directly to stdout, a FILE*, or a std::ostream, with mandatory Unicode-aware handling on all platforms.

Overview

std::print and std::println (C++23, <print>) are the recommended replacement for both printf and std::cout << chains. They share the {} format-string syntax introduced by std::format in C++20, but instead of returning a std::string, they write directly to the target stream β€” avoiding heap allocation for the common case of immediate output.

std::println appends a newline; std::println() with no arguments writes only a newline. Format strings are validated at compile time: passing the wrong argument type for a specifier is a hard error, not silent undefined behaviour.

Compiler requirements:

  • GCC 14+ (with libstdc++ or libc++)
  • Clang 17+ with libc++ (libstdc++ support varies by version)
  • MSVC 19.37+ (Visual Studio 2022 17.7+)
  • Flag: -std=c++23

For C++20, use std::format + std::fputs / stream insertion, or the {fmt} library whose API is nearly identical.

Syntax

cpp
// <print> β€” C++23
namespace std {

// stdout overloads
template<class... Args>
void print(format_string<Args...> fmt, Args&&... args);

template<class... Args>
void println(format_string<Args...> fmt, Args&&... args);

void println();  // writes '\n' only

// FILE* overloads
template<class... Args>
void print(FILE* stream, format_string<Args...> fmt, Args&&... args);

template<class... Args>
void println(FILE* stream, format_string<Args...> fmt, Args&&... args);

// std::ostream overloads
template<class... Args>
void print(ostream& os, format_string<Args...> fmt, Args&&... args);

template<class... Args>
void println(ostream& os, format_string<Args...> fmt, Args&&... args);

} // namespace std

format_string<Args...> is a class template (C++20) that captures the format string as a consteval parameter. This is what enables compile-time validation β€” you cannot pass a runtime std::string here directly (see Pitfalls).

Examples

Core formatting

cpp
#include <print>

// Basic substitution
std::println("Hello, {}!", "World");           // Hello, World!
std::println("x = {}, y = {:.4f}", 42, 3.14); // x = 42, y = 3.1400

// Positional arguments β€” zero-indexed
std::println("{1} before {0}", "B", "A");      // A before B

// Alignment and fill
std::println("{:>10}",   "right");   // "     right"
std::println("{:<10}",   "left");    // "left      "
std::println("{:^11}",   "center");  // "  center   "
std::println("{:*^11}",  "fill");    // "***fill****"

// Integer formatting
std::println("{:08d}", 42);          // "00000042"
std::println("{:+d}",  42);          // "+42"
std::println("{:#010x}", 255);       // "0x000000ff"
std::println("{:b}",   0b1101);      // "1101"
std::println("{:#b}",  0b1101);      // "0b1101"

// Floating-point
std::println("{:.2f}", 3.14159);     // "3.14"
std::println("{:e}",   12345.6);     // "1.234560e+04"
std::println("{:g}",   0.0001);      // "0.0001"
std::println("{:g}",   0.00001);     // "1e-05"

Writing to streams and files

cpp
#include <print>
#include <cstdio>
#include <fstream>

void log_error(const std::string& msg) {
    std::println(stderr, "[ERROR] {}", msg);  // FILE* overload
}

void write_report(const std::string& path, int count, double ratio) {
    FILE* f = std::fopen(path.c_str(), "w");
    if (!f) return;
    std::println(f, "Items processed: {}", count);
    std::println(f, "Success ratio:   {:.1f}%", ratio * 100.0);
    std::fclose(f);
}

// ostream overload β€” integrates with existing iostream infrastructure
void write_to_stream(std::ostream& os, int id, const std::string& name) {
    std::println(os, "{:04d}: {}", id, name);
}

Formatting ranges and custom types

Range formatting requires C++23 range support in std::format, which most major implementations shipped alongside <print>.

cpp
#include <print>
#include <vector>
#include <map>

std::vector<int> v{1, 2, 3, 4, 5};
std::println("{}", v);           // [1, 2, 3, 4, 5]
std::println("{:n}", v);         // 1, 2, 3, 4, 5    β€” 'n' suppresses brackets
std::println("{::06d}", v);      // [000001, 000002, 000003, 000004, 000005]

std::map<std::string, int> m{{"a", 1}, {"b", 2}};
std::println("{}", m);           // {a: 1, b: 2}

Custom types implement std::formatter (C++20):

cpp
struct Rect { double w, h; };

template<>
struct std::formatter<Rect> {
    constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }

    auto format(const Rect& r, std::format_context& ctx) const {
        return std::format_to(ctx.out(), "{}x{}", r.w, r.h);
    }
};

std::println("Canvas: {}", Rect{1920.0, 1080.0});  // Canvas: 1920x1080

Runtime format strings

std::print does not accept std::string as a format argument β€” use std::vprint_unicode or std::vprint_nonunicode for runtime strings:

cpp
#include <print>
#include <format>

std::string fmt = load_format_from_config();  // runtime string

// Runtime formatting to stdout β€” Unicode path
std::vprint_unicode(fmt, std::make_format_args(arg1, arg2));  // C++23

// Or: format to string first, then print
std::print("{}", std::vformat(fmt, std::make_format_args(arg1, arg2)));

Best Practices

Prefer std::println over manual '\n' management. It removes an entire class of bugs (forgotten newlines, flushing confusion) and is unambiguously clear about intent.

Use std::format when you need a std::string. If the result feeds into a log buffer, a network packet, or another API expecting a string, std::format is the right tool; std::print is for immediate output only.

cpp
// Building a log line for later use β€” use std::format, not std::println
std::string entry = std::format("[{}] {}: {}", timestamp, level, message);
log_buffer.push_back(std::move(entry));

For high-frequency paths, buffer explicitly. std::println writes per call. If you are emitting thousands of lines per second, use std::format_to into a pre-allocated buffer and flush in bulk:

cpp
std::string buf;
buf.reserve(65536);
for (const auto& record : records) {
    std::format_to(std::back_inserter(buf), "{}: {}\n", record.id, record.value);
}
std::fwrite(buf.data(), 1, buf.size(), stdout);

Do not mix std::print with std::cout on the same stream without care. std::print to stdout uses the underlying C FILE*; if std::cout is synced with stdio (the default), output ordering is preserved, but disabling sync (std::ios::sync_with_stdio(false)) can cause interleaving.

Common Pitfalls

Named arguments are not standard C++23. The "name"_a= syntax is a {fmt} library extension. Standard std::print supports only positional and indexed arguments:

cpp
// WRONG β€” not standard C++23
std::println("{name} = {value}", "name"_a = "x", "value"_a = 42);

// CORRECT β€” use indexed or default positional
std::println("{} = {}", "x", 42);
std::println("{0} = {1}", "x", 42);

Wrong type for a format specifier is a compile error, not silent truncation. This is intentional and desirable, but catches people used to printf:

cpp
std::println("{:d}", "hello");   // compile error β€” 'd' requires integer
std::println("{:.2f}", 42);      // compile error β€” precision on integer

std::format_to_n result fields are .out and .size, not a structured binding with ec:

cpp
char buf[64];
// WRONG
auto [ptr, ec] = std::format_to_n(buf, sizeof(buf), "val: {}", 42);

// CORRECT
auto result = std::format_to_n(buf, sizeof(buf) - 1, "val: {}", 42);
*result.out = '\0';  // null-terminate manually
// result.size = total chars that would have been written (may exceed limit)

Unicode on Windows requires a UTF-8 compatible locale or the console API path. std::print is required by the standard to use native Unicode APIs on Windows (e.g., WriteConsoleW) when writing to a console handle, so emoji and non-ASCII characters work without manual encoding conversion β€” unlike printf("%s", ...).

std::println() with no arguments is valid C++23 and writes only a newline. Do not confuse it with a bug; it is intentional for blank-line output.

std::format_to for High-Performance Output

std::format_to(OutputIt, format_string, args...) writes formatted output to any output iterator without heap-allocating a temporary std::string. This makes it the right tool whenever you are building up output incrementally or writing into a pre-existing buffer.

The most important case is pairing it with std::back_inserter for incremental string building. Reserve once, then loop:

cpp
// C++20
#include <format>
#include <string>

std::string build_table(const std::vector<Row>& rows) {
    std::string buf;
    buf.reserve(rows.size() * 64);  // one allocation up front

    for (const auto& row : rows) {
        std::format_to(std::back_inserter(buf),
                       "{:>6d}  {:<30s}  {:>10.2f}\n",
                       row.id, row.name, row.value);
    }
    return buf;
}

For writing into a fixed-size C buffer, use std::format_to_n. It returns a struct β€” .out points past the last written character and .size holds the total that would have been written, even if it exceeded the limit:

cpp
// C++20
char buf[128];
auto result = std::format_to_n(buf, sizeof(buf) - 1,
                               "sensor={}, temp={:.1f}Β°C", id, temp);
*result.out = '\0';  // null-terminate manually
// result.size >= bytes written; check if truncated:
if (result.size >= sizeof(buf)) { /* truncated */ }

When you only need the buffer size before writing (e.g., to allocate exactly), use std::formatted_size:

cpp
// C++20
std::size_t needed = std::formatted_size("{:>20s}: {:.4f}", label, value);
std::string s(needed, '\0');
std::format_to(s.begin(), "{:>20s}: {:.4f}", label, value);

format_to(back_inserter) is 2–5Γ— faster than format() + concatenation. The format() overload that returns std::string allocates a new string on every call; appending many of those with += compounds the cost. format_to into a reserved buffer writes directly and avoids every intermediate allocation.

Advanced std::formatter with Width, Fill, and Precision

A custom std::formatter can support the full format-spec mini-language by delegating spec parsing to an existing formatter. The key is storing a sub-formatter and forwarding to it in both parse() and format().

The following example makes Rect respect a width and precision spec like {:12.2}:

cpp
// C++20
#include <format>

struct Rect { double w, h; };

template<>
struct std::formatter<Rect> {
private:
    std::formatter<double> sub_;  // delegate spec handling to double

public:
    constexpr auto parse(std::format_parse_context& ctx) {
        // Let the double formatter consume the spec (width, precision, etc.)
        return sub_.parse(ctx);
    }

    auto format(const Rect& r, std::format_context& ctx) const {
        // Format width and height individually using the parsed spec
        std::format_to(ctx.out(), "(");
        sub_.format(r.w, ctx);
        std::format_to(ctx.out(), " Γ— ");
        sub_.format(r.h, ctx);
        return std::format_to(ctx.out(), ")");
    }
};

// Usage:
std::println("{:8.2f}", Rect{1920.0, 1080.0});  // (1920.00 Γ— 1080.00)

When the inner representation is a string, delegate to std::formatter<std::string> instead, and use dynamic width to forward alignment:

cpp
// C++20
template<>
struct std::formatter<MyId> {
private:
    std::formatter<std::string> sub_;

public:
    constexpr auto parse(std::format_parse_context& ctx) {
        return sub_.parse(ctx);
    }

    auto format(const MyId& id, std::format_context& ctx) const {
        // Build inner string, then let sub_ apply fill/align/width
        std::string inner = std::format("id:{:06d}", id.value);
        return sub_.format(inner, ctx);
    }
};

The simplest way to apply dynamic width inside a custom formatter is a nested format call. std::format_to(ctx.out(), "{:{}}", inner, width) re-enters the formatting engine with the computed width, avoiding manual fill/align arithmetic.

Dynamic Width and Precision

Width and precision can be taken from adjacent arguments at runtime by using a nested {} inside the format spec. The outer braces name the value; the inner braces name the width or precision argument:

cpp
// C++23
int width = 12;
std::println("{:{}}", "hello", width);       // "       hello"  (right-aligned, width 12)

double precision = 3;
std::println("{:.{}f}", 3.14159, precision); // "3.142"

// Both at once: value, then width, then precision
std::println("{:{}.{}f}", 3.14159, 10, 3);  // "     3.142"

This replaces printf's * wildcard (printf("%*d", width, value)) with a readable, type-safe equivalent. A common use case is right-aligning a table column whose width is computed from the data at runtime:

cpp
// C++23
void print_table(const std::vector<std::pair<std::string, int>>& rows) {
    // Determine the widest key at runtime
    std::size_t key_w = 0;
    for (const auto& [k, _] : rows) key_w = std::max(key_w, k.size());

    for (const auto& [k, v] : rows) {
        std::println("{:<{}}  {:>8d}", k, key_w, v);
    }
}

Dynamic width and precision are resolved left-to-right from the argument list. Both the value and the width/precision must be present as arguments; mixing positional indices with dynamic specs requires careful ordering.

std::format_string<Args...> as a Function Parameter

When writing a wrapper around std::print or std::format, you must preserve the compile-time format-string check. The way to do this is to accept std::format_string<Args...> as the first parameter β€” the same type the standard library uses internally:

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

template<typename... Args>
void log(std::format_string<Args...> fmt, Args&&... args) {
    std::print(stderr, "[LOG] ");
    std::println(stderr, fmt, std::forward<Args>(args)...);
}

// Compile-time validation is preserved:
log("x = {}", 42);           // OK
log("x = {:d}", "hello");    // compile error β€” type mismatch

Accepting std::string_view instead loses the compile-time check and forces you to drop down to the runtime vformat/vprint path:

cpp
// C++23 β€” runtime path; no compile-time validation
void log_runtime(std::string_view fmt, std::format_args args) {
    std::vprint_unicode(stderr, fmt, args);
}

A full logger class with level filtering that keeps compile-time validation intact:

cpp
// C++23
class Logger {
public:
    enum class Level { Debug, Info, Warn, Error };
    Level min_level = Level::Info;

    template<typename... Args>
    void info(std::format_string<Args...> fmt, Args&&... args) {
        emit(Level::Info, fmt, std::forward<Args>(args)...);
    }

    template<typename... Args>
    void error(std::format_string<Args...> fmt, Args&&... args) {
        emit(Level::Error, fmt, std::forward<Args>(args)...);
    }

private:
    template<typename... Args>
    void emit(Level lvl, std::format_string<Args...> fmt, Args&&... args) {
        if (lvl < min_level) return;
        std::println(stderr, "[{}] {}", level_name(lvl),
                     std::format(fmt, std::forward<Args>(args)...));
    }

    static constexpr 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";
        }
    }
};

C++26 note: std::basic_format_string<CharT, Args...> is the underlying template; std::format_string<Args...> is the char specialization and std::wformat_string<Args...> is the wchar_t specialization.

Performance: std::print vs printf vs std::cout

All three are I/O-bound when writing to a terminal, so differences only become measurable when writing to a buffered file or pipe. The following characterizes the mechanisms, not a single benchmark number β€” actual throughput depends heavily on libc, OS buffering, and record size.

printf writes to a FILE* with a thin formatting layer, minimal overhead, and no type safety. It is locale-dependent by default (affecting decimal separators, etc.).

std::cout << chains pay a virtual dispatch through std::streambuf for every << call. Each insertion is a separate virtual function call; complex output with many << operators can be 2–5Γ— slower than an equivalent printf call because of this overhead:

cpp
// C++11 β€” many virtual calls
std::cout << std::setw(8) << id << "  " << std::fixed
          << std::setprecision(2) << value << '\n';

// C++23 β€” one formatted write
std::println("{:8d}  {:.2f}", id, value);

std::print formats into an internal buffer, then issues a single fwrite (or platform Unicode API). Its throughput is close to printf for equivalent output, and consistently better than << chains for multi-field records.

Disabling stdio sync removes cout's penalty but risks interleaving with std::print. std::ios::sync_with_stdio(false) decouples cout's buffer from the C FILE* buffer. If you then also use std::print to stdout, the two buffers can interleave unpredictably. Pick one I/O path and stick to it.

For maximum throughput in log files or report generation, bypass the per-call flush and write in bulk:

cpp
// C++20 β€” maximum throughput pattern
std::string buf;
buf.reserve(1 << 20);  // 1 MiB up front

for (const auto& rec : records) {
    std::format_to(std::back_inserter(buf),
                   "{},{},{:.4f}\n", rec.ts, rec.id, rec.val);
}
// Single write β€” OS sees one large buffer
std::fwrite(buf.data(), 1, buf.size(), output_file);

Logging Framework Integration

std::print and std::format_to compose cleanly into a structured logging framework. The key design points are: keep compile-time format validation, format into a buffer (not directly to a stream) so you can prepend metadata, and guard concurrent writers.

cpp
// C++23
#include <format>
#include <print>
#include <syncstream>
#include <fstream>
#include <chrono>

class Logger {
public:
    enum class Level { Debug = 0, Info = 1, Warn = 2, Error = 3 };

    explicit Logger(std::ostream& sink, Level min = Level::Info)
        : sink_(sink), min_(min) {}

    template<typename... Args>
    void log(Level level, std::format_string<Args...> fmt, Args&&... args) {
        if (level < min_) return;

        // Format body at compile-time-validated call site
        std::string body = std::format(fmt, std::forward<Args>(args)...);

        // Prepend timestamp + level atomically via osyncstream
        std::osyncstream out(sink_);  // C++20: lock-free, atomic flush on destruction
        std::print(out, "[{}][{}] {}\n", timestamp(), level_str(level), body);
    }

    // Convenience wrappers
    template<typename... Args>
    void info(std::format_string<Args...> fmt, Args&&... args) {
        log(Level::Info, fmt, std::forward<Args>(args)...);
    }

    template<typename... Args>
    void error(std::format_string<Args...> fmt, Args&&... args) {
        log(Level::Error, fmt, std::forward<Args>(args)...);
    }

private:
    std::ostream& sink_;
    Level min_;

    static std::string timestamp() {
        auto now = std::chrono::system_clock::now();
        return std::format("{:%H:%M:%S}", now);
    }

    static constexpr std::string_view level_str(Level l) {
        switch (l) {
            case Level::Debug: return "DBG";
            case Level::Info:  return "INF";
            case Level::Warn:  return "WRN";
            case Level::Error: return "ERR";
        }
    }
};

std::osyncstream provides lock-free, contention-free concurrent logging without a mutex. Each thread writes to its own internal buffer; the osyncstream destructor flushes the entire message atomically to the underlying stream, preventing character-level interleaving between threads.

For structured (machine-readable) logs, format directly into JSON using format arguments as key-value pairs:

cpp
// C++23 β€” structured JSON log line
template<typename... Args>
void log_json(std::format_string<Args...> msg_fmt, Args&&... args) {
    std::string msg = std::format(msg_fmt, std::forward<Args>(args)...);
    std::println(R"({{"ts":"{}","level":"info","msg":"{}"}})",
                 timestamp(), msg);
}

Compile-time log-level filtering collapses filtered-out calls to nothing after inlining and dead-code elimination:

cpp
// C++23 β€” zero-cost filtering for disabled levels
template<Level MIN_LEVEL = Level::Info, typename... Args>
void log_static(Level level, std::format_string<Args...> fmt, Args&&... args) {
    if constexpr (level >= MIN_LEVEL) {  // evaluated at compile time when level is constexpr
        std::println("[{}] {}", level_str(level),
                     std::format(fmt, std::forward<Args>(args)...));
    }
}

See Also

  • std::format β€” construct a std::string from a format string without writing to a stream
  • std::formatter β€” customization point for user-defined types
  • Ranges β€” range formatting support used with {}