Skip to content

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
cpp
// 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):

text
这条进 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:

  1. Synchronization with C stdio (sync_with_stdio, enabled by default). To guarantee that std::cout and std::printf appear in the correct order, libstdc++ must coordinate with C's FILE* buffers on every << operation. This is a very real runtime cost.
  2. 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
cpp
// 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):

text
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 us

Want 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

code/examples/vol3/53_print_benchmark.cpp

Let's clarify the order of magnitude conclusions:

  • printf is 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 with printf/print (a pitfall discussed in the next section).
  • print consistently lands at 170–180 ms, achieving this speed without any sync switches, while providing type safety via format.
  • cout(sync=true) (default) is the slowest, around 180–190 ms—this is the actual cout performance 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:

cpp
// 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;
}
text
第一行(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:

cpp
// 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:

text
第一行(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:

cpp
// 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;
}
text
A(cout)
B(print→cout)
C(cout)
D(print→cout)

The order is completely correct. Comparing the two groups makes this clear:

text
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
cpp
// 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;
}
text
写 stderr: 2
写 stdout: 1
写 stringstream: x = 3.14
第二行
直接 print 到 cout: 7

The two sets of overloads correspond to two different underlying paths:

  • print(FILE*, …) uses C's fwrite and goes through the C stdio buffering. stdout is block-buffered (when redirected) or line-buffered (when in a terminal), while stderr is unbuffered.
  • print(ostream&, …) uses the ostream's streambuf and shares the buffer with <<. Writing to an ostringstream allows building strings in memory, while writing to cout shares 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 targeting stdout. This is the cleanest and fastest approach.
  • Mixing with existing cout code, with sync disabled: Use print(std::cout, ...) to guarantee ordering.
  • Accumulating formatted results in memory (e.g., constructing logs, serialization): Use print(oss, ...) to write to an ostringstream. This avoids the intermediate string construction required by oss << 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:

cpp
// 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:

text
=== 终端(行缓冲)输出 ===
[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:

cpp
// 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;
}
text
__cpp_lib_print = 202406
__cpp_lib_format = 202304
password

Here are a few important notes:

  • The value of __cpp_lib_print is 202406 in 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, seeing 202406 doesn't mean you have to compile with C++26; -std=c++23 is 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 with g++ -std=c++23, and it compiles and runs directly. For strict portability, write std::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_print and fall back to std::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):

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

int main()
{
    std::println("中文测试: 你好世界 π ≈ 3.14");
    std::println("emoji: 🚀 ✓ ★");
    return 0;
}
text
中文测试: 你好世界 π ≈ 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/println serves as the "direct output" layer paired with format in C++23. It bypasses the sync_with_stdio and locale overhead of iostream, writing directly to C streams.
  • Performance: In benchmarks writing two million short lines to /dev/null, printf is the fastest at ~130 ms, print is ~175 ms, cout(sync=false) is ~155–170 ms, and the default cout(sync=true) is the slowest at ~185 ms. print isn't the absolute champion, but it offers the best value in the "no sync, type-safe" category.
  • The Sync Trap: print(stdout) and cout coordinate via sync_with_stdio(true) by default, ensuring correct order. Once sync_with_stdio(false) is used, their buffers are decoupled, causing output order corruption (stably reproducible when redirecting to pipes/files). Solution: when mixing them, use print(std::cout, ...) to reuse cout's streambuf, which restores order.
  • Dual Overloads: print(FILE*, …) uses C stdio buffering, while print(ostream&, …) uses the ostream streambuf. Use stderr for logs (unbuffered) and ostringstream for string building.
  • Buffering Semantics: print writing to stdout is block-buffered and does not flush on \n (unlike endl). Abnormal program termination (abort/_exit/signals) will lose unflushed buffer contents. Critical logs should go to stderr or be manually flushed.
  • Compiler Support: GCC 16.1.1 with -std=c++23 offers full support, __cpp_lib_print = 202406 (C++23 DR). The parameterless println() is standard in C++26, but mainstream implementations provide it in C++23 mode. Older toolchains (GCC < 13) lack <print>; fall back to cout << format(...).
  • Unicode: With UTF-8 literal encoding, it takes the vprint_unicode path. It automatically converts on non-UTF-8 Windows terminals, making cross-platform Unicode output less hassle than printf.

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

v0.7.0-9-g940ec1b · 940ec1b · 2026-07-05