This chapter covers the two standard I/O facilities. The
I/O stream library (<iostream> and friends) provides
formatted and unformatted buffered I/O of text and numeric values; it is extensible to support
user-defined types exactly like built-in types, and it is type-safe. The
file system library (<filesystem>) provides basic
facilities for manipulating files and directories, built around the path type. An
ostream converts typed objects to a stream of characters; an
istream converts a stream of characters to typed objects. The operations are
type-safe, type-sensitive, and extensible to user-defined types (§11.5).
Thread running through this chapter: the stream is a stateful converter
between typed objects and characters. Every sharp corner is a state question:
>> skips leading whitespace (but is.get(c) doesn't); the
stream's error state (fail, eof) is how you detect ill-formed
input; formatting manipulators are "sticky" (while format() specifiers are
not); streams can't be copied (move-only — pass by reference); and I/O from multiple threads
is a data race unless synchronized with osyncstream. The interview story is:
use format() (C++20, type-safe) over printf, check your file
streams opened, and treat input as untrusted data.
The I/O stream library provides formatted and unformatted buffered I/O of text and numeric
values. It is extensible to support user-defined types exactly like built-in types and is
type-safe. The operations on
istreams and ostreams are type-safe, type-sensitive, and extensible
to handle user-defined types (§11.5). Other forms of user interaction (graphical I/O) are
handled through libraries that are not part of the ISO standard and are not described here.
The streams can be used for binary I/O, for a variety of character types, be locale-specific,
and use advanced buffering strategies — topics beyond the scope of this book. The I/O stream
classes all have destructors that free all resources owned (buffers, file handles): they are
examples of "Resource Acquisition Is Initialization" (RAII, §6.3).
In <ostream>, the I/O stream library defines output for every built-in
type, and it's easy to define output for a user-defined type (§11.5). The operator
<< ("put to") is the output operator on ostream objects;
cout is the standard output stream and cerr
the standard stream for reporting errors. By default, values written to
cout are converted to a sequence of characters:
cout << 10; // places the characters '1' and '0' on the standard output stream
int x {10};
cout << x; // same output
// Chaining: the result of an output expression can be used for further output:
void h2(int i) { cout << "the value of i is " << i << '\n'; } // "the value of i is 10"
A character is output as a character, not as a numerical value:
int b = 'b'; // char implicitly converted to int: 98 (ASCII)
char c = 'c';
cout << 'a' << b << c; // outputs: a98c
In <istream>, the standard library offers istreams for input.
The operator >> ("get from") is the input operator; cin is the
standard input stream. The type of the right-hand operand of
>> determines what input is accepted and where it goes:
int i;
cin >> i; // read an integer into i
double d;
cin >> d; // read a double-precision floating-point number into d
Input operations chain like output operations: cin >> i >> d;. The
read of an integer is terminated by any character that is not a digit. By default,
>> skips initial whitespace, so 1234 12.34e5
is a suitable input sequence.
Reading a sequence of characters into a string stops at whitespace by default —
entering Eric Bloodaxe still yields Hello, Eric!. To read a whole
line, use getline():
string str;
getline(cin, str); // reads the whole line; the terminating newline is discarded
Using the formatted I/O operations is usually less error-prone, more efficient, and less code
than manipulating characters one by one —
istreams take care of memory management and range checking. The standard strings
expand to hold what you put in them: you don't have to pre-calculate a maximum size, so string
input never overflows.
An iostream has a state we can examine to determine whether an operation
succeeded. The most common use is reading a sequence of values:
vector<int> read_ints(istream& is) {
vector<int> res;
for (int i; is>>i; ) // read until something that is not an integer (typically end of input)
res.push_back(i);
return res;
}
The operation is>>i returns a reference to is, and testing an
iostream yields true if the stream is ready for another operation.
The I/O state holds everything needed to read or write: formatting information, error state
(e.g., has end-of-input been reached?), and buffering kind. We can set the state to reflect
that an error occurred (§11.5) and clear it if the error wasn't serious:
vector<int> read_ints(istream& is, const string& terminator) {
vector<int> res;
for (int i; is >> i; ) res.push_back(i);
if (is.eof()) // fine: end of file
return res;
if (is.fail()) { // we failed to read an int; was it the terminator?
is.clear(); // reset the state to good()
string s;
if (is>>s && s==terminator) return res;
is.setstate(ios_base::failbit); // add fail() to is's state
}
return res;
}
auto v = read_ints(cin, "stop");
The iostream library lets us define I/O for our own types. Consider an
Entry for a telephone book:
struct Entry { string name; int number; };
ostream& operator<<(ostream& os, const Entry& e) {
return os << "{\"" << e.name << "\", " << e.number << "}";
}
A user-defined output operator takes its output stream (by reference) as its first argument and returns it as its result — so chaining keeps working. The corresponding input operator is more complicated because it has to check for correct formatting and deal with errors:
istream& operator>>(istream& is, Entry& e) // read { "name" , number } pair
{
char c, c2;
if (is>>c && c=='{' && is>>c2 && c2=='"') { // start with a { followed by a "
string name;
while (is.get(c) && c!='"') // anything before a " is part of the name
name += c;
if (is>>c && c==',') {
int number = 0;
if (is>>number>>c && c=='}') { // read the number and a }
e = {name, number}; // assign to the entry
return is;
}
}
}
is.setstate(ios_base::failbit); // register the failure in the stream
return is;
}
An input operation returns a reference to its istream that can be used to test
success — used as a condition, is>>c means "did we succeed in reading a
char from is into c?" Note the whitespace subtlety:
is>>c skips whitespace by default, but is.get(c) does not — so
this input operator ignores whitespace outside the name string, but not within it. Reading
{ "John Marwood Cleese", 123456 } via
for (Entry ee; cin>>ee; ) cout << ee << '\n';
round-trips the entry. For a more systematic pattern-recognition technique, see regular
expressions (§10.4).
The iostream and format libraries provide operations for controlling
the format of input and output. The iostream facilities are about as old as C++ and focus on
formatting streams of numbers; the format facilities (§11.6.2) are recent (C++20) and focus on
printf()-style (§11.8) specification of combinations of values.
The simplest formatting controls are called manipulators and are found in
<ios>, <istream>, <ostream>, and
<iomanip> (for manipulators that take arguments):
cout << 1234 << ' ' << hex << 1234 << ' ' << oct << 1234 << dec << 1234 << '\n';
// 1234 4d2 2322 1234
constexpr double d = 123.456;
cout << d << "; " // default format
<< scientific << d << "; " // 1.123e2 style
<< hexfloat << d << "; " // hexadecimal notation
<< fixed << d << "; " // 123.456 style
<< defaultfloat << d << '\n'; // back to default
// 123.456; 1.234560e+002; 0x1.edd2f2p+6; 123.456000; 123.456
Precision is an integer determining the number of digits used to display a floating-point
number. The general format (defaultfloat) lets the implementation choose a style
that best preserves the value in the space available, with precision as the
maximum number of digits. The scientific format presents one digit before the decimal
point plus an exponent, with precision as the maximum digits after the point. The
fixed format presents an integer part, decimal point, and fractional part, precision again the
digits after the point. Values are rounded, not truncated, and
precision() doesn't affect integer output:
cout.precision(8);
cout << 1234.56789 << ' ' << 1234.56789 << ' ' << 123456 << '\n'; // 1234.5679 1234.5679 123456
cout.precision(4);
cout << 1234.56789 << ' ' << 1234.56789 << ' ' << 123456 << '\n'; // 1235 1235 123456
These floating-point manipulators are "sticky": their effects persist for
subsequent floating-point operations — they're designed for formatting streams of values. We
can also specify the field size and alignment of a number. In addition,
<< handles time and dates (duration, time_point,
year_month_day, weekday, month,
zoned_time; §16.2), complex numbers, bitsets, error codes, and
pointers.
It has been credibly argued that printf() is the most popular function in C and a
significant factor in its success — but it suffers from a lack of type safety and a lack of
extensibility to user-defined types. In <format>, the standard library
provides a type-safe, though not extensible, printf()-style
mechanism. The basic function format() produces a string:
string s = format("Hello, {}\n", val); // if val is "World": "Hello, World\n"
Ordinary characters in the format string go straight into the output;
{} takes the next argument and prints it with its << default.
A formatting directive is preceded by a colon: {:x} hexadecimal,
{:o} octal, {:d} decimal, {:b} binary (not directly
supported by ostream):
cout << format("{} {:x} {:o} {:d} {:b}\n", 1234,1234,1234,1234,1234);
// 1234 4d2 2322 1234 10011010010
By default format() takes arguments in order, but we can specify an arbitrary
order — and format an argument more than once:
cout << format("{3:} {1:x} {2:o} {0:b}\n", 000, 111, 222, 333); // 333 6f 336 0
cout << format("{0:} {0:x} {0:o} {0:d} {0:b}\n", 1234); // default, hex, octal, decimal, binary
The number before the colon is the argument index; numbering starts at zero. The ability to
place arguments out of order is highly praised by people composing messages in different
natural languages. The floating-point formats are the same as for ostream:
e scientific, a hexfloat, f fixed,
g default. A dot precedes a precision specifier:
cout << format("precision(8): {:.8} {} {}\n", 1234.56789, 1234.56789, 123456);
// precision(8): 1234.5679 1234.56789 123456 — unlike streams, specifiers are NOT sticky
format() offers a mini-language of about 60 format specifiers for very detailed
control over numbers and dates; all time and date format strings start with %. If
a formatting error is caught at run time, a format_error exception is thrown:
string ss = format("{:%F}", 2); // error: mismatched argument (potentially caught at compile time)
string sss = format("{%F}", 2); // error: bad format (potentially caught at compile time)
The constant formats above can be checked at compile time. The complementary
vformat() takes a variable as its format — more flexibility, more run-time
errors:
string fmt = "{}";
cout << vformat(fmt, make_format_args(2)); // OK
fmt = "{:%F}";
cout << vformat(fmt, make_format_args(2)); // error: format and argument mismatch, caught at run time
Finally, format_to(back_inserter(buf), "iterator: {} {}\n", "Hi! ", 2022)
writes directly into a buffer defined by an iterator — interesting for performance when using
a stream's buffer directly or some other output device.
The standard library directly supports:
We can also define our own streams, e.g., attached to communication channels.
Streams cannot be copied — they are move-only; always pass them by reference.
All standard-library streams are templates parameterized on character type:
ostream is basic_ostream<char>, with a wide-character version
wostream (basic_ostream<wchar_t>) for Unicode.
cout for "ordinary output"; cerr for unbuffered "error
output"; clog for buffered "logging output"; cin for
standard input.
In <fstream>: ifstream for reading from a file,
ofstream for writing, fstream for both. Testing that a file stream
was properly opened is usually done by checking its state:
ofstream ofs {"target"}; // "o" for "output"
if (!ofs) error("couldn't open 'target' for writing");
ifstream ifs {"source"}; // "i" for "input"
if (!ifs) error("couldn't open 'source' for reading");
Once opened, ofs behaves like an ordinary ostream (just like
cout) and ifs like an ordinary istream. File
positioning and detailed open control are beyond this book's scope.
In <sstream>: istringstream (read from a string),
ostringstream (write to a string), stringstream (both). The contents
of an ostringstream can be read with str() (a string copy) or
view() (a string_view). One common use is formatting before giving the result to
a GUI; a string received from a GUI can be parsed by putting it into an
istringstream. A stringstream supports general string-based
conversion:
template<typename Target = string, typename Source = string>
Target to(Source arg) // convert Source to Target
{
stringstream buf;
Target result;
if (!(buf << arg) // write arg into stream
|| !(buf >> result) // read result from stream
|| !(buf >> std::ws).eof()) // is anything left in stream?
throw runtime_error{"to<>() failed"};
return result;
}
auto x1 = to<string,double>(1.2); // very explicit (and verbose)
auto x2 = to<string>(1.2); // Source deduced to double
auto x3 = to<>(1.2); // Target defaulted to string; Source deduced
auto x4 = to(1.2); // the <> is redundant: all args defaulted/deduced
Stroustrup calls this "a good example of the generality and ease of use that can be achieved by a combination of language features and standard-library facilities."
Streams attached to user-designated memory have existed since the earliest days of C++: the
old strstream has been deprecated for decades, but its replacement —
spanstream, ispanstream, and ospanstream — won't be
official before C++23 (already widely available in implementations):
void user(int arg) {
array<char,128> buf;
ospanstream ss(buf); // takes a span rather than a string
ss << "write " << arg << " to memory\n";
// ...
}
Attempts to overflow the target buffer set the stream state to failure (§11.4).
In a multi-threaded system, I/O becomes an unreliable mess unless only one thread uses the
stream, or access is synchronized so that only one thread at a time gains access. An
osyncstream guarantees that a sequence of output operations completes
and its results appear in the output buffer as expected, even if another thread tries to
write:
void unsafe(int x, string& s) {
cout << x; // a different thread may introduce a data race (§18.2) between these
cout << s;
}
void safer(int x, string& s) {
osyncstream oss(cout); // the two writes complete as a group
oss << x;
oss << s;
}
Other threads that also use osyncstreams won't interfere — but a thread using
cout directly could. Either use osyncstream consistently or make
sure only a single thread produces output to a specific stream. Concurrency is tricky (Chapter
18): avoid data sharing between threads whenever feasible.
The C++ standard library also supports the C standard-library I/O, including
printf() and scanf(). Many uses of this library are unsafe from a
type and security point of view; Stroustrup doesn't recommend its use. It's difficult to use
for safe and convenient input, and it does not support user-defined types. If you don't use
C-style I/O but care about performance, call
ios_base::sync_with_stdio(false); // avoid significant overhead
Without that call, the standard iostreams (e.g., cin and cout) can
be significantly slowed down to stay compatible with C-style I/O. If you like
printf()-style formatted output, use format (§11.6.2): it's
type-safe, easier to use, as flexible, and as fast.
Most systems have a notion of a file system providing access to permanent information stored
as files — but their properties and manipulation methods vary greatly. The file system library
in
<filesystem> offers a uniform interface
to most facilities of most file systems. Using it, we can portably express file system paths
and navigate through a file system, and examine file types and permissions.
path f = "dir/hypothetical.cpp"; // naming a file
assert(exists(f)); // f must exist
if (is_regular_file(f)) // is f an ordinary file?
cout << f << " is a file; its size is " << file_size(f) << '\n';
A program manipulating a file system usually runs alongside other programs, so the contents
can change between two commands: even though we asserted f existed, that may no
longer be true on the very next line. A path is a quite complicated class,
handling the varied character sets and conventions of many operating systems — including
command-line file names from main(). A path is
not checked for validity until it is used; even then, validity depends on the
conventions of the system the program runs on. A path can be used to open a file:
ofstream f {p}; — and tested: if (!f) error("bad file name: ", p);.
A string is implicitly converted to a path.
Types for traversing directories: path (a directory path),
filesystem_error (a file system exception), directory_entry,
directory_iterator, recursive_directory_iterator. Listing a
directory:
void print_directory(path p) // print the names of all files in p
try {
if (is_directory(p)) {
cout << p << ":\n";
for (const directory_entry& x : directory_iterator{p})
cout << " " << x.path() << '\n';
}
}
catch (const filesystem_error& ex) { cerr << ex.what() << '\n'; }
Use recursive_directory_iterator{p} to also list subdirectories; copy the paths
into a vector and sort to print in lexicographical order. Among the useful
path operations (with p, p2 paths):
| Operation | Meaning |
|---|---|
p=p2 |
Assign p2 to p |
p/=p2 |
p and p2 concatenated using the file-name separator (by default /) |
p+=p2 |
p and p2 concatenated (no separator) |
s=p.string() / p.generic_string() |
p in the native / generic format as a string |
p2=p.filename() / p.stem() / p.extension() |
The filename / stem / extension part of p |
i=p.begin() / i=p.end() |
Iterate over p's element sequence |
p==p2, p<p2 … |
Equality and lexicographical comparisons |
is>>p, os<<p |
Stream I/O to/from p |
u8path(s) |
A path from a UTF-8 encoded source |
A typical use — examining files in a directory:
void test(path p) {
if (is_directory(p)) {
for (const directory_entry& x : directory_iterator(p)) {
const path& f = x; // refer to the path part of a directory entry
if (f.extension() == ".exe")
cout << f.stem() << " is a Windows executable\n";
else {
string n = f.extension().string();
if (n == ".cpp" || n == ".C" || n == ".cxx")
cout << f.stem() << " is a C++ source file\n";
}
}
}
}
Naming conventions, natural languages, and string encodings are rich in complexity — the standard-library filesystem abstractions offer portability and great simplification.
The standard library offers a small set of file operations implementable on a wide variety of
systems: exists(p), copy(p1,p2), copy_file(p1,p2),
create_directory(p) (intermediate directories must exist),
create_directories(p) (creates all intermediates),
current_path() (get or set), file_size(p),
remove(p) (file or empty directory). Many operations have overloads taking extra
arguments such as permissions.
Like copy(), all operations come in two versions:
exists(p) — throws filesystem_error if
the operation failed.
error_code argument, e.g., exists(p,e) —
check e to see if the operation succeeded.
Use the error-code versions when operations are expected to fail frequently in normal use; the
throwing versions when an error is considered exceptional. The library knows a few common
kinds of files and classifies the rest as "other":
is_block_file, is_character_file, is_directory,
is_empty, is_fifo, is_other,
is_regular_file, is_socket, is_symlink,
status_known.
Here is a summary of the guidance from this chapter. All 30 items, with the section where each is introduced. The C++ Core Guidelines link each item to its recommended practice.
| # | Guideline | § |
|---|---|---|
| 1 | iostreams are type-safe, type-sensitive, and extensible. | 11.1 |
| 2 | Use character-level input only when you have to. | 11.3 |
| 3 | When reading, always consider ill-formed input. | 11.3 |
| 4 |
Avoid endl (if you don't know what endl is, you haven't missed
anything).
|
— |
| 5 |
Define << and >> for user-defined types with values
that have meaningful textual representations.
|
11.1 |
| 6 | Use cout for normal output and cerr for errors. |
11.1 |
| 7 | There are iostreams for ordinary and wide characters, and you can define an iostream for any kind of character. | 11.1 |
| 8 | Binary I/O is supported. | 11.1 |
| 9 | There are standard iostreams for standard I/O streams, files, and strings. | 11.2 |
| 10 | Chain << operations for a terser notation. |
11.2 |
| 11 | Chain >> operations for a terser notation. |
11.3 |
| 12 | Input into strings does not overflow. | 11.3 |
| 13 | By default >> skips initial whitespace. |
11.3 |
| 14 |
Use the stream state fail to handle potentially recoverable I/O errors.
|
11.4 |
| 15 |
We can define << and >> operators for our own types.
|
11.5 |
| 16 |
We don't need to modify istream or ostream to add new
<< and >> operators.
|
11.5 |
| 17 | Use manipulators or format() to control formatting. |
11.6.1 |
| 18 |
precision() specifications apply to all following floating-point output
operations.
|
11.6.1 |
| 19 |
Floating-point format specifications (e.g., scientific) apply to all
following floating-point output operations.
|
11.6.1 |
| 20 |
#include <ios> or <iostream> when using standard
manipulators.
|
11.6 |
| 21 | Stream formatting manipulators are "sticky" for use for many values in a stream. | 11.6.1 |
| 22 |
#include <iomanip> when using standard manipulators taking arguments.
|
11.6 |
| 23 | We can output time, dates, etc. in standard formats. | 11.6.1 |
| 24 | Don't try to copy a stream: streams are move only. | 11.7 |
| 25 | Remember to check that a file stream is attached to a file before using it. | 11.7.2 |
| 26 | Use stringstreams or memory streams for in-memory formatting. | 11.7.3 |
| 27 | We can define conversions between any two types that both have string representation. | 11.7.3 |
| 28 | C-style I/O is not type-safe. | 11.8 |
| 29 |
Unless you use printf-family functions, call
ios_base::sync_with_stdio(false).
|
11.8 |
| 30 |
Prefer <filesystem> to direct use of platform-specific interfaces.
|
11.9 |
What do an ostream and an istream each convert, and what makes
the operations type-safe?
What is the difference between cin >> str,
getline(cin, str), and is.get(c) regarding whitespace?
In read_ints(is, "stop"), why must the function call
is.clear() before reading the terminator string, and what does
setstate(ios_base::failbit) do?
What must a user-defined operator<< and
operator>> each do, and why is is.get(c) (not
is>>c) used to read the Entry's name?
What does it mean that floating-point manipulators like scientific and
precision() are "sticky", and what is the contrast with
format()?
What does format("{3:} {1:x} {2:o} {0:b}\n", 000, 111, 222, 333) output, and
what does the number before the colon mean?
When is a format_error thrown, and what does
vformat(fmt, make_format_args(...)) add that format() lacks?
Why can't you copy a stream, and what does that mean for function parameters?
After ofstream ofs {"target"}; why test if (!ofs), and what do
str() and view() each return on an ostringstream?
Why is
void unsafe(int x, string& s) { cout << x; cout << s; }
unsafe in a multi-threaded program, and how does osyncstream fix it?
What is ios_base::sync_with_stdio(false) for, and when should you NOT call
it?
Why can a file system change "between two commands", and when is a path's
validity actually checked?
ostream ⇒ typed objects → chars ; istream
⇒ chars → typed objects
type-safe + type-sensitive + extensible ⇒ user types like built-ins (§11.5)
RAII ⇒ destructors free buffers + file handles
move-only ⇒ pass by reference →
operator<</>> take ostream&/istream&
templates ⇒ basic_ostream<char> | wostream
→ wide/unicode
cout normal | cerr unbuffered errors |
clog buffered logging | cin input
<</>> chain ⇒ return stream ref
char output ⇒ as char, not number (eg 'a' << 98 →
a98)
>> ⇒ skips initial whitespace ; stops at whitespace
→ use getline for lines
is.get(c) ⇒ does not skip whitespace
string input ⇒ grows → no overflow ; no pre-calc size
is>>i ⇒ returns is ; testing stream ⇒ true
if ready
eof() ⇒ end reached | fail() ⇒ failed
read
clear() ⇒ reset to good → continue after recoverable error
setstate(ios_base::failbit) ⇒ deliberately mark failure
operator<<(ostream&, const T&) ⇒ return stream ref
operator>>(istream&, T&) ⇒ check format char-by-char ;
setstate(failbit) on failure
whitespace ⇒ >> skips v/s get(c) doesn't
→ names keep spaces
manipulators ⇒ hex/oct/dec ;
scientific/hexfloat/fixed/defaultfloat ; <iomanip> for
arg-taking
sticky ⇒ persist for following floats ; designed for value streams
precision ⇒ general = max digits ; sci/fixed = digits after point ; rounds, not truncates ; × integers
field size + alignment specifiable
C++20 type-safe printf ⇒ <format> ; × extensible to
user types
{} next arg ; {:x} hex {:o} oct {:d} dec
{:b} binary
{3:} ⇒ index (zero-based) → reorder + repeat
{0:} {0:x}
floats ⇒ e sci a hexfloat f fixed
g default ; {:.8} precision → not sticky
time/date ⇒ specifiers start with % ; ~60 specifiers
format_error ⇒ mismatch at run time
(compile-time if constant)
vformat(fmt, make_format_args(...)) ⇒ variable format, run-time only
format_to(back_inserter(buf), ...) ⇒ write into iterator buffer
standard ⇒ cout/cerr/clog/cin
file ⇒ ifstream/ofstream/fstream →
check state after open if (!ofs)
string ⇒ istringstream/ostringstream/stringstream ;
str() copy | view() string_view
memory ⇒ spanstream C++23 (strstream deprecated) ; overflow →
failure state
sync ⇒ osyncstream groups output ops → no interleave ;
× direct cout user
printf/scanf ⇒ not type-safe ; × user-defined types
ios_base::sync_with_stdio(false) ⇒ remove stdio-compat overhead
(× if mixing)
prefer format() ⇒ type-safe, easy, flexible, fast
<filesystem> ⇒ uniform interface, portable
path ⇒ not validated until used ; fs changes between commands
→ re-check
/= ⇒ concat with separator | += without ;
filename()/stem()/extension() ; u8path()
iteration ⇒ directory_iterator |
recursive_directory_iterator ; directory_entry
ops ⇒
exists/copy/copy_file/create_directories/current_path/file_size/remove
two versions ⇒ throwing filesystem_error |
error_code& for expected failures
types ⇒ is_regular_file, is_directory,
is_symlink … rest = is_other
Primary source: Stroustrup, B. (2022). A Tour of C++, 3rd ed.,
Chapter 11: "Input and Output." Addison-Wesley.
Reference:
Chapter 1–11 Quick Reference & Glossary —
keep it beside you while you study.
Recommended supplement: cppreference on
I/O library,
std::format,
std::filesystem, and
sync_with_stdio.
Questions? Ask your agent — your teacher — about anything unclear: why a stream is move-only,
when to use error codes instead of filesystem exceptions, or how
osyncstream differs from locking cout. Follow-ups are expected, not
optional.