print: C++23 Decoupled Output and iostream
std::format (C++20) solved the problem of "how to stitch data into a string," but left an awkward tail end: to get that formatted string onto the screen, we still had to hand it back to std::cout << std::format(...). This detour gives back the performance gains that format worked so hard to save—cout carries the entire weight of iostream synchronization and locale mechanisms, making every << call expensive. std::print / std::println (C++23) are here to finish the job: they combine formatting and output into a single call, writing directly to the stream, bypassing the iostream << chain entirely.
In this post, we focus on the output semantics of print—why it is faster than cout, the classic ordering chaos when mixing it with cout, the orders of magnitude we see in real-world benchmarks, and how to choose between the FILE* and ostream overloads. We won't repeat format string syntax (like {}, {:x}, {:.3f}) here, as that belongs to the previous post on std::format. Here, we only care about "how the result gets out, if it gets out fast, and if it fights with other output methods."
Let's Try It Out
The basic look of print is almost identical to format, with the only difference being that it writes the result directly to stdout instead of returning a string:
Expand (23 lines)Collapse
// Standard: C++23
#include <print>
#include <cstdio>
int main()
{
// 重载 (1):直接写 stdout
std::print("Hello, {}\n", "world");
std::print("{2} {1}{0}!\n", 23, "C++", "Hello"); // 手动索引,可以乱序
// println:末尾自动加换行,不用自己补 \n
std::println("一行带换行: {}", 42);
// 带 format-spec 的格式串(语法细节归 format 那篇)
std::println("十六进制: {:x} 浮点: {:.3f}", 255, 3.14159265);
// 转义花括号
std::println("字典字面量: {{key: {}}}", "value");
// print 到 stderr(stderr 无缓冲,行不会卡)
std::println(stderr, "这条进 stderr");
return 0;
}Here is the output generated by g++ -std=c++23 -O2 (local GCC 16.1.1):
这条进 stderr
Hello, world
Hello C++23!
一行带换行: 42
十六进制: ff 浮点: 3.142
字典字面量: {key: value}Note the first line—the content from stderr actually appears at the very top. This isn't a bug; it is precisely the core mechanism we want to explain: println(stderr, ...) writes to the unbuffered stderr, so it lands immediately. Meanwhile, print writes to stdout, which (when redirected to a pipe or an editor's output window) is fully buffered and waits until the program finishes to flush everything at once. The two streams use their own separate buffers, so who lands first depends on who isn't buffering. This "separate buffering" is exactly the root cause of the ordering confusion we will encounter later.
Why print is faster than cout: bypassing two layers of overhead
To understand the design motivation behind print, we first need to look at why std::cout is slow. With a statement like std::cout << "i=" << i << '\n', every << operator carries two specific burdens:
- Synchronization with C stdio (
sync_with_stdio, enabled by default). To guarantee thatstd::coutandstd::printfappear in the correct order, libstdc++ must coordinate with C'sFILE*buffers on every<<operation. This is a very real runtime cost. - Locale awareness. Formatting in iostream (numbers, currency, dates) requires checking the locale. Even if you have never set a locale, this check still executes.
std::print bypasses both of these layers. It calls C's FILE* write functions directly (using the fwrite path for stdout) and uses the compile-time parsing results from std::format for formatting, completely bypassing iostream. cppreference describes it simply as "equivalent to std::print(stdout, fmt, args...)", with the underlying implementation landing on direct stream writing functions like vprint_unicode or vprint_nonunicode.
In other words, print carries none of the baggage that cout does: "synchronization + locale + a function call for every operator". This is the literal meaning of its design goal to "decouple from iostream".
However, there is a counterintuitive point we must clarify first: print is not the "fastest output method forever". Its value lies not in being the absolute fastest in speed, but in "achieving speeds close to printf without disabling sync or touching locale, while retaining format's type safety". The benchmark in the next section will make this clear—don't let the slogan "print is fast" mislead you.
Real-world test: print vs. cout vs. printf
Simply saying "bypassing overhead" isn't enough; let's go straight to a benchmark. We wrote two million short lines using cout (with sync on and off), printf, and print to /dev/null (to exclude terminal I/O noise and measure only the formatting and buffering logic), timing the results down to the microsecond:
Expand (62 lines)Collapse
// Standard: C++23
#include <cstdio>
#include <iostream>
#include <print>
#include <chrono>
constexpr int kIterations = 2'000'000;
static void report(const char* name, std::chrono::steady_clock::time_point t0,
std::chrono::steady_clock::time_point t1)
{
auto us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
std::fprintf(stderr, "%-22s %lld us\n", name, (long long)us);
}
void bench_cout_sync()
{
std::ios::sync_with_stdio(true); // 默认
auto t0 = std::chrono::steady_clock::now();
for (int i = 0; i < kIterations; ++i) {
std::cout << "i=" << i << " sq=" << i * 2 << '\n';
}
report("cout (sync=true)", t0, std::chrono::steady_clock::now());
}
void bench_cout_nosync()
{
std::ios::sync_with_stdio(false); // 常见的"加速 cout"写法
auto t0 = std::chrono::steady_clock::now();
for (int i = 0; i < kIterations; ++i) {
std::cout << "i=" << i << " sq=" << i * 2 << '\n';
}
report("cout (sync=false)", t0, std::chrono::steady_clock::now());
}
void bench_printf()
{
auto t0 = std::chrono::steady_clock::now();
for (int i = 0; i < kIterations; ++i) {
std::printf("i=%d sq=%d\n", i, i * 2);
}
report("printf", t0, std::chrono::steady_clock::now());
}
void bench_print()
{
auto t0 = std::chrono::steady_clock::now();
for (int i = 0; i < kIterations; ++i) {
std::print("i={} sq={}\n", i, i * 2);
}
report("print", t0, std::chrono::steady_clock::now());
}
int main()
{
std::freopen("/dev/null", "w", stdout); // 排除终端 I/O 噪声
bench_cout_sync();
bench_printf();
bench_print();
bench_cout_nosync();
return 0;
}Native GCC 16.1.1, -std=c++23 -O2, running three times (absolute microsecond values fluctuate with load; we focus on the order of magnitude and relative relationships):
cout (sync=true) 180151 us
printf 128323 us
print 176287 us
cout (sync=false) 150366 us
cout (sync=true) 179359 us
printf 133809 us
print 171167 us
cout (sync=false) 155003 us
cout (sync=true) 191023 us
printf 133643 us
print 176044 us
cout (sync=false) 171143 usWant to run it yourself? Check out this online demo (compiled with -std=c++23, timing output sent to stderr):
Compiler Explorer
cout / printf / std::print Formatting Performance Comparison
Writing 2 million short lines to /dev/null, comparing formatting overhead of cout (sync on/off), printf, and std::print—print achieves top-tier speed without touching sync
Let's clarify the order of magnitude conclusions:
printfis the fastest (approx. 130 ms), but the cost is C-style variadic arguments—type unsafe, and mismatched format strings and arguments lead to runtime crashes.cout(sync=false)comes next (approx. 155–170 ms), but this requires manually disabling synchronization. Once disabled, the standard library no longer guarantees output ordering withprintf/print(a pitfall discussed in the next section).printconsistently lands at 170–180 ms, achieving this speed without any sync switches, while providing type safety viaformat.cout(sync=true)(default) is the slowest, around 180–190 ms—this is the actualcoutperformance most people get without changing settings.
So, the honest conclusion is: print is not the absolute speed champion (printf and cout with sync disabled can tie or even be faster). However, it offers the best value when you "don't want to touch sync but want type-safe formatting." If your code is already full of cout and you've disabled sync for performance, swapping a few hot path lines to print might not yield gains—print's main battleground is new code and scenarios where you want to completely move away from iostreams.
The Real Pitfall: Mixing print and cout Causes Ordering Issues
Performance is a selling point of print, but the biggest daily stumbling block is synchronization. print writes directly to C's stdout buffer, while cout (in libstdc++) has its own streambuf. The two are coordinated by default via sync_with_stdio(true), so the order is correct by default:
// Standard: C++23
#include <iostream>
#include <print>
int main()
{
// sync_with_stdio 默认 true,顺序正确
std::cout << "第一行(cout)\n";
std::print("第二行(print)\n");
std::cout << "第三行(cout)\n";
std::print("第四行(print)\n");
return 0;
}第一行(cout)
第二行(print)
第三行(cout)
第四行(print)However, many people write std::ios::sync_with_stdio(false) for performance. Once this line is added, the synchronization between cout and the C stdio is broken. Both buffers flush independently, and the order immediately becomes garbled:
// Standard: C++23
#include <iostream>
#include <print>
int main()
{
std::ios::sync_with_stdio(false); // 常见的"加速 cout"写法
std::cout << "第一行(cout)\n";
std::print("第二行(print)\n");
std::cout << "第三行(cout)\n";
std::print("第四行(print)\n");
std::cout.flush(); // 不 flush,cout 残留可能根本看不到
return 0;
}Redirect output to a pipe (block buffered mode) to reliably reproduce:
第一行(cout)
第三行(cout)
第二行(print)
第四行(print)The two lines from cout rushed to the front, while the two lines from print got squeezed at the back—this is clearly not the order in the source code. The reason is exactly as stated above: after disabling sync, cout's streambuf and the C stdout buffer used by print are no longer coordinated. Whoever fills up first, or gets flushed first, lands first. Here, cout is flushed uniformly at program termination, so its content is dumped as a chunk at the end (though its internal relative order remains correct).
Don't mix print(stdout) and cout after disabling sync
sync_with_stdio(false) acts as a "global switch"—once turned off, it affects the relationship between cout/cin and C stdio throughout the entire program. If you disable sync in a performance-critical section, but then use both cout and print (which defaults to stdout) elsewhere, the output order is not guaranteed. This interleaving might coincidentally look fine in a terminal (line buffering), but it will consistently break when redirected to a file or pipe (block buffering). Debugging this kind of bug is excruciating, because "the order is correct on my machine."
The Fix: Use print(cout, …) to Share the Same Buffer
There is a very clean solution to this pitfall, provided you know that print isn't limited to writing to FILE*—C++23 provides an ostream overload. By switching print's target from the default stdout to cout itself, the output goes through cout's streambuf and shares the same buffer as <<, naturally restoring the correct order:
// Standard: C++23
#include <iostream>
#include <print>
int main()
{
std::ios::sync_with_stdio(false);
std::cout << "A(cout)\n";
std::print(std::cout, "B(print→cout)\n"); // 走 cout 自己的缓冲
std::cout << "C(cout)\n";
std::print(std::cout, "D(print→cout)\n");
std::cout.flush();
return 0;
}A(cout)
B(print→cout)
C(cout)
D(print→cout)The order is completely correct. Comparing the two groups makes this clear:
print(stdout) 与 cout 交错(sync=false): → A C B D (错乱)
print(cout) 与 << 同缓冲(sync=false): → A B C D (正确)The Mechanism in a Nutshell: print(FILE*) and cout use independent buffers. When synchronization is disabled, they flush independently. print(ostream), however, reuses cout's streambuf and shares its fate with the << operator. Therefore, there is a simple rule of thumb for engineering—if your program disables synchronization, always use print(std::cout, ...) when mixing I/O, and avoid bare print(...). This approach gives us the formatting convenience of print without causing conflicts with cout.
The Two Faces of print: FILE* vs. ostream
The solution discussed in the previous section relies on the fact that print has two overloads for writing to streams. This detail is easily overlooked, but it is worth clarifying on its own, as it directly determines whether we can "take a detour around the pitfall."
Expand (21 lines)Collapse
// Standard: C++23
#include <iostream>
#include <sstream>
#include <print>
int main()
{
// 一副面孔:FILE*(stdout/stderr/自己 fopen 的文件)
std::print(stdout, "写 stdout: {}\n", 1);
std::println(stderr, "写 stderr: {}", 2); // stderr 无缓冲,适合日志/错误
// 另一副面孔:ostream(cout/cerr/stringstream/任何 ostream)
std::ostringstream os;
std::print(os, "写 stringstream: {} = {}\n", "x", 3.14);
std::println(os, "第二行");
std::print("{}", os.str()); // 把攒下来的内容一次性吐到 stdout
// 当然也能直接喂 cout
std::print(std::cout, "直接 print 到 cout: {}\n", 7);
return 0;
}写 stderr: 2
写 stdout: 1
写 stringstream: x = 3.14
第二行
直接 print 到 cout: 7The two sets of overloads correspond to two different underlying paths:
print(FILE*, …)uses C'sfwriteand goes through the C stdio buffering.stdoutis block-buffered (when redirected) or line-buffered (when in a terminal), whilestderris unbuffered.print(ostream&, …)uses the ostream'sstreambufand shares the buffer with<<. Writing to anostringstreamallows building strings in memory, while writing tocoutshares the buffer with<<(the solution from the previous section).
How do we choose in practice? Here are three guidelines:
- Pure new code, performance-first: Use
print(...)/println(...)directly targetingstdout. This is the cleanest and fastest approach. - Mixing with existing
coutcode, with sync disabled: Useprint(std::cout, ...)to guarantee ordering. - Accumulating formatted results in memory (e.g., constructing logs, serialization): Use
print(oss, ...)to write to anostringstream. This avoids the intermediate string construction required byoss << std::format(...).
Printing to stderr and buffering semantics
print writes to stdout by default, but logs and error messages more commonly target stderr. This is directly supported, and it offers a natural convenience: stderr is unbuffered, so data is written immediately upon each call. Using println(stderr, ...) for logging ensures we don't have to worry about logs getting stuck in a buffer if the program crashes.
However, note that this "unbuffered" benefit applies only to stderr. When print writes to stdout, it is block-buffered (when redirected to a file or pipe), and it does not flush just because it encounters a \n—this differs from the semantics of std::endl (which triggers a flush). A comparison makes this obvious:
// Standard: C++23
#include <print>
#include <cstdlib>
int main()
{
std::print("第一行(带换行)\n");
std::print("第二行没换行就崩了");
std::abort(); // 模拟崩溃
}The results differ when running directly in the terminal versus running inside a pipe:
=== 终端(行缓冲)输出 ===
[exit=134] # 整段都没出来,abort 前缓冲没刷
=== 重定向到管道(块缓冲)输出 ===
[done] # 同样什么都没有In both cases, the content buffered by print is lost due to abort() (which does not flush user-space buffers and calls _exit directly)—including the first line with the \n. This indicates that print on stdout does not flush on newlines; it relies on a unified flush when the program exits normally. If your program might exit abnormally (crash, _exit, or killed by a signal) and you want to ensure critical logs are persisted, either write to stderr (unbuffered) or manually call std::fflush(stdout) at critical points.
print does not flush on newline like endl
The endl in std::cout << ... << std::endl conveniently flushes the stream, leading many to mistakenly believe that "outputting a newline flushes the buffer." print does not exhibit this behavior—it writes to stdout using block buffering, where \n is just a regular character. When a forced flush is necessary, use std::flush on the corresponding stream, or simply send critical output to the unbuffered stderr. Bugs involving lost logs during crashes are, nine times out of ten, rooted here.
Compiler Support and Feature-testing
std::print and std::println are C++23 features found in the <print> header. The local GCC 16.1.1 offers full support, which is clearly visible via the feature-test macro:
// Standard: C++23
#include <print>
int main()
{
#ifdef __cpp_lib_print
std::println("__cpp_lib_print = {}", __cpp_lib_print);
#endif
#ifdef __cpp_lib_format
std::println("__cpp_lib_format = {}", __cpp_lib_format);
#endif
// println() 无参重载:标准里算 C++26,但 cppreference 注明
// "all known implementations make them available in C++23 mode"
std::print("password");
std::println(); // 只输出换行
return 0;
}__cpp_lib_print = 202406
__cpp_lib_format = 202304
passwordHere are a few important notes:
- The value of
__cpp_lib_printis202406in GCC 16. This value actually corresponds to a C++23 Defect Report (DR) that backported "unbuffered formatted output" and support for more formattable types into C++23. So, seeing202406doesn't mean you have to compile with C++26;-std=c++23is sufficient. - Parameterless
std::println()(which only outputs a newline) is technically added in C++26 by the standard, but mainstream implementations (GCC, Clang, and MSVC) already provide it in C++23 mode. I tested locally withg++ -std=c++23, and it compiles and runs directly. For strict portability, writestd::print("\n"), but this detail isn't critical. - Older GCC versions (before 13) lack
<print>. If your code needs to compile on older toolchains, either wrap it in#ifdef __cpp_lib_printand fall back tostd::cout << std::format(...), or pull in the {fmt} library.
Unicode Output: The Extra Work print Does
print also handles something printf ignores: Unicode terminal adaptation. Its equivalent implementation on cppreference splits into two paths: if the ordinary literal encoding is UTF-8, it takes vprint_unicode; otherwise, it takes vprint_nonunicode. This split isn't just for show. Historically, the Windows console default code page was not UTF-8 (legacy systems used GBK/CP437). print handles conversion internally to ensure UTF-8 content displays correctly on Windows terminals, rather than spewing garbage characters. Linux and macOS terminals are natively UTF-8, so this path is essentially a pass-through.
Tested locally (where ordinary literal encoding is UTF-8):
// Standard: C++23
#include <print>
int main()
{
std::println("中文测试: 你好世界 π ≈ 3.14");
std::println("emoji: 🚀 ✓ ★");
return 0;
}中文测试: 你好世界 π ≈ 3.14
emoji: 🚀 ✓ ★This is quite important for cross-platform code—while both output Unicode, printf on Windows requires you to manually call SetConsoleOutputCP(CP_UTF8) and deal with the hassle, whereas print encapsulates this for you. However, note that this assumes your source file literals are actually UTF-8 encoded (so the compile-time vprint_unicode path is active); if your source file is GBK but you want to use the Unicode path, you must ensure encoding consistency yourself—print cannot magically "convert" non-UTF-8 literals into UTF-8.
Summary
Let's wrap up the print suite:
- Positioning:
print/printlnserves as the "direct output" layer paired withformatin C++23. It bypasses thesync_with_stdioand locale overhead of iostream, writing directly to C streams. - Performance: In benchmarks writing two million short lines to
/dev/null,printfis the fastest at ~130 ms,printis ~175 ms,cout(sync=false)is ~155–170 ms, and the defaultcout(sync=true)is the slowest at ~185 ms.printisn't the absolute champion, but it offers the best value in the "no sync, type-safe" category. - The Sync Trap:
print(stdout)andcoutcoordinate viasync_with_stdio(true)by default, ensuring correct order. Oncesync_with_stdio(false)is used, their buffers are decoupled, causing output order corruption (stably reproducible when redirecting to pipes/files). Solution: when mixing them, useprint(std::cout, ...)to reusecout's streambuf, which restores order. - Dual Overloads:
print(FILE*, …)uses C stdio buffering, whileprint(ostream&, …)uses the ostream streambuf. Usestderrfor logs (unbuffered) andostringstreamfor string building. - Buffering Semantics:
printwriting tostdoutis block-buffered and does not flush on\n(unlikeendl). Abnormal program termination (abort/_exit/signals) will lose unflushed buffer contents. Critical logs should go tostderror be manually flushed. - Compiler Support: GCC 16.1.1 with
-std=c++23offers full support,__cpp_lib_print = 202406(C++23 DR). The parameterlessprintln()is standard in C++26, but mainstream implementations provide it in C++23 mode. Older toolchains (GCC < 13) lack<print>; fall back tocout << format(...). - Unicode: With UTF-8 literal encoding, it takes the
vprint_unicodepath. It automatically converts on non-UTF-8 Windows terminals, making cross-platform Unicode output less hassle thanprintf.
In the next article, we will shift to the other side of text processing and explore deeper features of the format library—specializing formatters for custom types, and formatting ranges/pairs/tuples—taking the print/format ecosystem from "usable" to "customizable".
References
- cppreference: std::print (C++23) —
printFILE*overloads, equivalence withstdout/vprint_unicode, and feature-test macros - cppreference: std::println (C++23) —
printlnoverloads, the parameterless version, and its relationship with C++26 - cppreference: std::print(std::ostream) (C++23) —
ostreamoverload, reusing streambuf, and sharing buffers with<< - cppreference: sync_with_stdio — Semantics and performance impact of the iostream and C stdio synchronization switch