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