API
printf
uses a replacement-based API with placeholders starting with %
replaced with formatted arguments:
printf("The answer is %d.\n", answer);
cout
or, more generally, ostreams use a concatenation-based API with parts of a formatted message interleaved with arguments:
std::cout << "The answer is " << answer << ".\n";
A replacement-based API with proper synchronization provides atomicity, e.g. when writing from multiple threads different messages written with printf
won't interleave while parts of the messages written with cout
may interleave. For this reason C++20 introduced std::osyncstream
which is a clunky way of achieving atomicity.
With operator overloading formatting can quickly become cumbersome, e.g.
std::cout << std::setprecision(2) << std::fixed << 1.23456 << "\n";
vs
printf("%.2f\n", 1.23456);
Matthew Wilson, the author of FastFormat, called this "chevron hell".
Extensibility
cout
supports formatting of user-defined types through overloading of operator<<
. There is no standard way to do the same with printf
, although there is a glibc extension which is rarely used in practice.
Safety
printf
uses varargs which are inherently unsafe unless you use something like GCC's format
attribute which only works with literal format strings. It is a user's responsibility to correctly pass type information via format specifiers. Any mismatch results in an undefined behavior and is a common source of vulnerabilities.
cout
/ostreams are type-safe and the user doesn't need to manually handle type information.
Buffering
cout
adds another layer of buffering and synchronizes with the underlying C streams by default. This brings significant performance overhead. It is possible to disable this synchronization at the cost of worse interoperability with C and potentially other languages.
Formatting state
In printf
formatting is controlled via a format string and decoupled from the stream itself. In cout
/ostreams the formatting state is stored in the stream which may negatively affect performance and cause unexpected results. Quoting N4412: Shortcomings of iostreams:
Formatting parameters (such as uppercase/lowercase and radix) are specified by setting flags, which mostly persist for an arbitrary number of subsequent low-level formatting operations, until explicitly changed. This approach inhibits compile-time checks and compile-time choice of formatting, and potentially establishes state shared between threads (which requires synchronization for access).
Locales
printf
uses the global C locale.
cout
uses the C++ locale associated with the stream.
Both printf
and cout
use locales by default.
Performance
cout
is often slower than printf
for reasons mentioned above: extra buffering and synchronization (can be disabled) and stateful API.
Language
printf
is a part of the C standard library and can be used in C and C++. cout
is a part of the C++ standard library and can only be used in C++.
You can have the best of both worlds by using C++23 std::print
. It provides a replacement-based API with positional arguments. It is extensible, type-safe, doesn't introduce extra buffering and makes localized formatting an opt-in.
Disclaimer: I'm the author of C++23 std::print
.