Physical Isolation

This section explains how type-erased wrappers enable compilation firewalls and transport-independent APIs.

Prerequisites

The Compilation Firewall Pattern

C++ templates are powerful but have a cost: every instantiation compiles in every translation unit that uses it. Change a template, and everything that includes it recompiles.

Type-erased wrappers break this dependency:

// protocol.hpp - No template dependencies
#pragma once
#include <boost/capy/io/any_stream.hpp>
#include <boost/capy/task.hpp>

// Declaration only - no implementation details
task<> handle_protocol(any_stream& stream);
// protocol.cpp - Implementation isolated here
#include "protocol.hpp"
#include <boost/capy/read.hpp>
#include <boost/capy/write.hpp>

task<> handle_protocol(any_stream& stream)
{
    char buf[1024];

    for (;;)
    {
        auto [ec, n] = co_await stream.read_some(mutable_buffer(buf));
        if (ec.failed())
            co_return;

        // Process and respond...
        co_await write(stream, make_buffer(response));
    }
}

Changes to protocol.cpp only recompile that file. The header is stable.

Build Time Benefits

Before (Templates Everywhere)

// Old approach: template propagates everywhere
template<typename Stream>
task<> handle_protocol(Stream& stream);

// Every caller instantiates for their stream type
// Changes force recompilation of all callers

After (Type Erasure at Boundary)

// New approach: concrete signature
task<> handle_protocol(any_stream& stream);

// Implementation compiles once
// Callers only depend on the signature

Measured Impact

For a typical project:

  • Template-heavy design: 10+ seconds incremental rebuild

  • Type-erased boundaries: < 1 second incremental rebuild

The difference grows with project size.

Transport Independence

Type erasure decouples your code from specific transport implementations:

// Your library code
task<> send_message(any_write_sink& sink, message const& msg)
{
    co_await sink.write(make_buffer(msg.header));
    co_await sink.write(make_buffer(msg.body), true);
}

Callers provide any conforming implementation:

// TCP socket
tcp::socket socket;
any_write_sink sink{socket};
send_message(sink, msg);

// TLS stream
tls::stream stream;
any_write_sink sink{stream};
send_message(sink, msg);

// HTTP chunked encoding
chunked_sink chunked{underlying};
any_write_sink sink{chunked};
send_message(sink, msg);

// Test mock
test::write_sink mock;
any_write_sink sink{mock};
send_message(sink, msg);

Same send_message function, different transports—compile once, use everywhere.

API Design Guidelines

Accept Type-Erased References

// Good: accepts any stream
task<> process(any_stream& stream);

// Avoid: forces specific type
task<> process(tcp::socket& socket);

Wrap at Call Site

void caller(tcp::socket& socket)
{
    any_stream stream{socket};  // Wrap here
    process(stream);            // Call with erased type
}

The wrapper creation is explicit and localized.

Return Concrete Types (Usually)

// OK: factory returns concrete type
tcp::socket create_socket();

// Then caller wraps if needed
auto socket = create_socket();
any_stream stream{socket};

Returning type-erased values forces heap allocation. Return concrete types when the caller knows what they need.

Example: Library API

// http_client.hpp
#pragma once
#include <boost/capy/io/any_read_source.hpp>
#include <boost/capy/io/any_write_sink.hpp>

struct http_request
{
    std::string method;
    std::string url;
    std::map<std::string, std::string> headers;
};

struct http_response
{
    int status_code;
    std::map<std::string, std::string> headers;
    any_read_source body;  // Body is a source, not a buffer
};

// Send request, receive response
// Works with any transport that provides any_stream
task<http_response> send_request(any_stream& conn, http_request const& req);

Users don’t need to know how HTTP is implemented:

// User code
tcp::socket socket;
// ... connect ...

any_stream conn{socket};
auto response = co_await send_request(conn, {
    .method = "GET",
    .url = "/api/data"
});

// Read body through type-erased source
flat_dynamic_buffer buf;
co_await read(response.body, buf);

The HTTP library is isolated from transport details. It compiles once. Users bring their own transport.

Wrapper Overhead

Type erasure has runtime cost:

  • Virtual dispatch for each operation

  • Extra indirection through wrapper

But the cost is typically negligible compared to I/O latency. A nanosecond of dispatch overhead is invisible next to microsecond network operations.

When profiling shows wrapper overhead matters:

  1. Consider batching operations

  2. Use concrete types in hot paths

  3. Accept the template cost for that code path

Reference

Type-erased wrappers are in <boost/capy/io/>:

  • any_stream

  • any_read_stream, any_write_stream

  • any_read_source, any_write_sink

  • any_buffer_source, any_buffer_sink

You have now completed the Stream Concepts section. These abstractions—streams, sources, sinks, and their type-erased wrappers—form the foundation for Capy’s I/O model. Continue to Example Programs to see complete working examples.