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:

--- config: theme: default fontFamily: Lato, proxima-nova, "Helvetica Neue", Arial, sans-serif --- classDiagram class Path { +content() PathContent } class PathContent { +openByteInputStream(PathReadDataOptions) ByteInputStreamPtr +openByteOutputStream(PathWriteDataOptions) ByteOutputStreamPtr +openTextInputStream(PathReadTextOptions) TextInputStreamPtr +openTextOutputStream(PathWriteTextOptions) TextOutputStreamPtr } Path --> PathContent : content()
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.

--- config: theme: default fontFamily: Lato, proxima-nova, "Helvetica Neue", Arial, sans-serif --- classDiagram class PathReadDataOptions { +maximumByteLength +timeout +streamSettings } class PathReadTextOptions { +encoding +bomMode +encodingMode +maximumByteLength +maximumCpLength +timeout +streamSettings } class PathWriteDataOptions { +createParents +creationMode +accessProfile +timeout +streamSettings } class PathWriteTextOptions { +createParents +creationMode +accessProfile +encoding +bomMode +timeout +streamSettings }

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.

createParents

Creates missing parent directories before opening an output file. Leave it disabled when a missing directory should reveal a configuration or spelling error.

creationMode

PathCreateMode::CreateNew is the safe default and fails if the path already exists. CreateOrOverwrite replaces existing content, while CreateOrAppend preserves it and writes at the end. Append streams are deliberately not positionable.

accessProfile

Selects 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.

encoding

Selects 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.

bomMode

Controls 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.

encodingMode

Applies 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.

maximumByteLength and maximumCpLength

Protect the whole-file readData... and readText... convenience methods from unbounded allocation. They are intentionally ignored by open...InputStream() because stream callers control their own incremental limits.

timeout and streamSettings

setTimeout() 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.

--- config: theme: default fontFamily: Lato, proxima-nova, "Helvetica Neue", Arial, sans-serif --- classDiagram class InputStreamSettings { +timeout +buffering } class OutputStreamSettings { +timeout +buffering +backBufferLimit }
timeout

Sets 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.

buffering

Expresses the intended balance between memory use and throughput using MinimalMemory, Interactive, Balanced, Throughput, or Bulk. The library selects suitable sizes independently for native rings, aggregate reads, decoders, and output retention.

backBufferLimit

Sets 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 InputStream or OutputStream base only for code that deals solely with lifecycle, settings, or readiness and does not read or write content.

  • Do not accept a Path or 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

Manage Shared Ownership Deliberately

Library factories return std::shared_ptr aliases such as ByteInputStreamPtr and TextOutputStreamPtr. Copying one of these pointers does not copy the file or create a second cursor: every alias refers to the same stream, state, position, settings, and queued data. The native resource remains alive until the last shared owner releases it.

Shared ownership solves lifetime, not coordination. Callers still need a clear operation owner, a rule for which component may read or write, and one place that performs the final close. Concurrent producers must also inspect every result; a pointer copy does not make a sequence of operations atomic.

The standard stream proxies are process-wide objects with additional rules, and coroutine operations retain ownership while suspended. Those cases are covered in Standard Streams and Coroutine Streams.

Where to Continue

With creation and ownership established, choose the guide that matches the data boundary:

Then read Readiness and Timeouts and Stream Lifecycle before building a long-running operation or a shutdown path that must preserve output reliably.