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.

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