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++23Type-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
// <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 stdformat_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
#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
#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>.
#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):
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: 1920x1080Runtime format strings
std::print does not accept std::string as a format argument β use std::vprint_unicode or std::vprint_nonunicode for runtime strings:
#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.
// 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:
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:
// 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:
std::println("{:d}", "hello"); // compile error β 'd' requires integer
std::println("{:.2f}", 42); // compile error β precision on integerstd::format_to_n result fields are .out and .size, not a structured binding with ec:
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:
// 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:
// 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:
// 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}:
// 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:
// 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:
// 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:
// 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:
// 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 mismatchAccepting std::string_view instead loses the compile-time check and forces you to drop down to the runtime vformat/vprint path:
// 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:
// 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:
// 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:
// 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.
// 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:
// 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:
// 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 astd::stringfrom a format string without writing to a streamstd::formatterβ customization point for user-defined types- Ranges β range formatting support used with
{}