Creating Streams and Managing Ownership
This page shows how to select a byte or text interface, open file-backed streams through
Path, configure them before the first operation, and pass them safely through an
application.
The goal is to keep components independent of a particular destination while making ownership and cleanup explicit.
Choose a Narrow Stream Interface
The stream API deliberately separates byte from text data and input from output.
Choose the interface at the boundary where your application decides what the data means.
A binary inspector needs ByteInputStreamPtr because the encoded bytes
matter; a document reader needs TextInputStreamPtr because it works
with decoded Unicode.
The following example opens the same UTF-8 file twice. The two visible functions receive only the interface they need: one exposes the exact file bytes as hexadecimal, while the other receives decoded text.
/// Accept a byte input stream when a component must work with the exact file representation.
/// This function can read any byte source; it neither knows nor cares that the caller opened a file.
void printFileAsHex(const el::ByteInputStreamPtr &input) {
const auto result = input->readAll(el::ByteLength{1024U});
if (result.hasData()) {
// The UTF-8 encoding of “Ö” is visible as the final two bytes.
el::io::printLine("Bytes: "_el, el::ByteFormat::separated(), result.data());
}
}
/// Accept a text input stream when a component needs decoded Unicode characters.
/// The same function also works with temporary, standard, redirected, or custom text input streams.
void printFileAsText(const el::TextInputStreamPtr &input) {
const auto result = input->readAll(el::CpLength{1024U});
if (result.hasData()) {
// The stream decoder turns the file encoding into an Erbsland Core string.
el::io::printLine("Text: "_el, result.data());
}
}
Bytes: 4d 6f 74 69 76 3a 20 c3 96 6c
Text: Motiv: Öl
Open File Streams Through a Path
Use Path::content() to open a stream.
It returns a lightweight PathContent facade with four stream-opening methods:
openByteInputStream()Opens the file for exact byte input. Use it for binary formats or when decoding is handled by another component.
openByteOutputStream()Opens the file for exact byte output. The write options decide whether the file must be new, is replaced, or is opened for append.
openTextInputStream()Opens a byte file and adds Unicode decoding. Its options select UTF-8, UTF-16, or UTF-32, BOM behavior, and recovery from invalid input.
openTextOutputStream()Opens a byte file and adds Unicode encoding. Callers write characters and strings; the stream produces the configured encoded representation.
All four methods return a shared pointer to an already open stream.
The result combines the native file handle, buffering, optional text encoding, positioning when supported, and a
path-aware error source.
Opening failures such as a missing file or rejected access throw PathError;
failures after the stream is open surface as StreamError.
For small files that must be handled as one value, PathContent also provides readData(), readText(),
writeData(), and writeText() plus throwing OrThrow variants.
These are whole-file convenience operations, not stream-opening methods.
Use a stream when data can be large, work must be incremental, or timeout and readiness states must remain visible.
Why Explicit Close Matters
A successful output write means that the complete request was accepted, but bytes may still be queued.
OutputStream::close() gracefully drains output, closes the native
file, and gives the application a chance to observe timeout or failure.
If close times out, the stream remains in Closing and the owner can call it again.
Destroying the last shared owner does not perform a potentially blocking graceful close. Destruction follows the immediate abort path so it is safe during stack unwinding and shutdown; queued output may be abandoned and no close failure can be reported to the caller. Therefore, every owned output file should have an explicit close point on its successful path.
Input streams have no queued application output, so destruction can safely abort them. Closing input explicitly is still useful: it stops pending work, releases the native handle at a known point, and makes it clear that no later component may continue reading through another shared alias.
/// Open file-backed streams through `Path::content()`.
/// The path factory returns a shared stream with the platform handle, buffering, encoding, and error context already
/// connected. Always finish owned output with `close()` so queued data reaches the file.
void openFileStreams() {
const auto directory = createStreamDemoDirectory("skizzen"_el);
const auto path = directory->path() / "studie.txt"_el;
// Write a small UTF-8 sketchbook entry.
const auto output = path.content().openTextOutputStream();
output->writeLine("Studie 17: Licht auf dem Nordhang"_el);
output->close();
// Open the same path for decoded text input.
const auto input = path.content().openTextInputStream();
const auto result = input->readAll();
if (result.hasData()) {
el::io::print(result.data());
}
}
Studie 17: Licht auf dem Nordhang
Configure Before Opening
Opening is the point where filesystem policy, text encoding, time bounds, and memory limits become fixed. The defaults are useful for ordinary UTF-8 files, but configure explicitly when an existing file may be replaced or appended, parent directories may be absent, content is sensitive, another encoding is required, or the stream must meet specific latency and memory bounds.
Path Options
The four option classes mirror the four opening methods.
The diagram lists their configurable properties; each property has a matching getter and chainable set...() method.
Sensitive File Input
Enable sensitivity in the nested stream settings before opening the stream.
The setting protects library-owned byte rings and decoder storage, and generic text reads return marked UTF-8 strings.
For byte streams, the ordinary owned read(), readExact(), readAll(), and coroutine APIs automatically return
marked byte blocks when this setting is enabled.
auto streamSettings = el::InputStreamSettings{};
streamSettings.setSensitive(true);
auto options = el::PathReadTextOptions{};
options.setStreamSettings(streamSettings);
const auto input = path.content().openTextInputStream(options);
const auto result = input->readAll(el::CpLength{4096U});
if (result.hasData()) {
useSecretText(result.data());
}
The sensitivity policy is carried by the stream settings rather than a separate read-method family. Converting the returned string to another width or an external representation creates an ordinary unmarked value.
createParentsCreates missing parent directories before opening an output file. Leave it disabled when a missing directory should reveal a configuration or spelling error.
creationModePathCreateMode::CreateNewis the safe default and fails if the path already exists.CreateOrOverwritereplaces existing content, whileCreateOrAppendpreserves it and writes at the end. Append streams are deliberately not positionable.accessProfileSelects a portable access intent for newly created files: platform default, current user only, user and group, or everyone. It does not rewrite permissions of a file that already exists.
encodingSelects UTF-8, UTF-16, or UTF-32 and, for the latter two, automatic or explicit little/big-endian byte order. Byte stream options do not have an encoding because they preserve the exact representation.
bomModeControls whether a byte-order mark is accepted or emitted automatically, required, or rejected. This is especially important when generic UTF-16 or UTF-32 must discover its byte order.
encodingModeApplies only to text input and chooses whether invalid encoded data throws or is replaced with the Unicode replacement character. Text output assumes strings use the intended representation; only transcoding repairs malformed data with the replacement character.
maximumByteLengthandmaximumCpLengthProtect the whole-file
readData...andreadText...convenience methods from unbounded allocation. They are intentionally ignored byopen...InputStream()because stream callers control their own incremental limits.timeoutandstreamSettingssetTimeout()is the concise way to change only the per-operation wait.setStreamSettings()supplies the complete input or output settings object described below.
Stream Settings
Path options contain one immutable settings object that is copied into the new stream. After opening, every shared user sees the same time and memory policy.
timeoutSets the maximum wait for one public operation. It is not a deadline for the whole file: a loop may make progress through many individually bounded operations.
bufferingExpresses the intended balance between memory use and throughput using
MinimalMemory,Interactive,Balanced,Throughput, orBulk. The library selects suitable sizes independently for native rings, aggregate reads, decoders, and output retention.backBufferLimitSets the hard limit for queued output beyond the fixed front buffer. It bounds memory and determines the largest atomic request the stream can accept under back pressure.
The following example combines filesystem options, UTF-16 encoding, BOM policy, and output buffering before opening the file.
/// Configure a stream before opening it.
/// File options combine filesystem policy with immutable stream settings, so every user of the resulting stream sees
/// the same timeout, buffering, creation mode, encoding, and byte-order-mark behavior.
void configureAtCreation() {
auto streamSettings = el::OutputStreamSettings{};
streamSettings.setTimeout(el::TimeDelta::milliseconds(250))
.setBuffering(el::StreamBuffering::Throughput)
.setBackBufferLimit(el::ByteLength{128U * 1024U});
auto options = el::PathWriteTextOptions{el::StringEncoding::Utf16LittleEndian};
options.setCreationMode(el::PathCreateMode::CreateOrOverwrite)
.setBomMode(el::StringBomMode::Require)
.setStreamSettings(streamSettings);
const auto directory = createStreamDemoDirectory("konfiguration"_el);
const auto output = (directory->path() / "farbenlehre.txt"_el).content().openTextOutputStream(options);
output->writeLine("Ultramarin neben warmem Ocker"_el);
output->close();
el::io::printLine("Encoding configured: UTF-16 LE"_el);
el::io::printLine("Back-buffer limit: "_el, streamSettings.backBufferLimit().toSizeT(), " bytes"_el);
}
Encoding configured: UTF-16 LE
Back-buffer limit: 131072 bytes
Design Functions Around Stream Interfaces
A function that consumes or produces data should normally depend on a stream interface, not on the path, temporary file, terminal, or string builder behind it. This makes the same logic reusable in production, tests, command-line tools, and in-memory capture.
Choose the narrowest abstraction that provides every operation the function uses:
Start with direction: input or output.
Choose bytes when the representation matters and text when the function works with Unicode characters or lines.
Use the generic
InputStreamorOutputStreambase only for code that deals solely with lifecycle, settings, or readiness and does not read or write content.Do not accept a
Pathor concrete stream implementation unless the function truly needs path-specific behavior or must open the stream itself.
For a synchronous helper, pass the shared pointer alias by const reference. If a component stores the stream or starts work that outlives the call, store a pointer copy so ownership is explicit. Ordinary helpers should not close or abort a borrowed stream: the owner of the complete operation should do that after all producers or consumers have finished.
/// Pass shared stream interfaces to components that produce or consume data.
/// Library-created streams use shared ownership so decorators and coroutine operations can safely keep their backing
/// stream alive. The caller that owns the complete operation remains responsible for flushing or closing the stream.
void shareStreamWithWriter() {
const auto output = el::AnyStringBuilderStream::create();
// The helper depends only on text output, not on a particular destination.
writeStudyLabel(output, "Birken im Morgennebel"_el);
writeStudyLabel(output, "Felsen nach dem Regen"_el);
el::io::print(output->takeString());
}
Motiv: Birken im Morgennebel
Motiv: Felsen nach dem Regen
Where to Continue
With creation and ownership established, choose the guide that matches the data boundary:
Byte Input and Output for binary records, bounded block loops, exact reads, atomic writes, and endian-aware integers.
Text Input, Output, and Encoding for Unicode blocks, lines, formatting, output capture, encodings, BOMs, and encoding errors.
Then read Readiness and Timeouts and Stream Lifecycle before building a long-running operation or a shutdown path that must preserve output reliably.