Physical Isolation
This section explains how type-erased wrappers enable compilation firewalls and transport-independent APIs.
Prerequisites
-
Completed Streams (Partial I/O)
-
Understanding of type-erased wrappers
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
// Fragments deliberately leave results and bindings unused; the pages
// explain the values in prose instead.
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-value"
#pragma GCC diagnostic ignored "-Wunused-result"
#pragma GCC diagnostic ignored "-Wunused-function"
// gcc 15 with sanitizers misattributes coroutine frame delete paths
#pragma GCC diagnostic ignored "-Wmismatched-new-delete"
#endif
#if defined(__clang__)
#pragma clang diagnostic ignored "-Wunused-lambda-capture"
#pragma clang diagnostic ignored "-Wunused-private-field"
#endif
#if defined(_MSC_VER)
#pragma warning(disable: 4834) // discarding [[nodiscard]] return value
#pragma warning(disable: 4189) // local variable initialized but not referenced
#pragma warning(disable: 4100) // unreferenced formal parameter
#pragma warning(disable: 4101) // unreferenced local variable
#pragma warning(disable: 4456) // declaration hides previous local declaration
#pragma warning(disable: 4457) // declaration hides function parameter
#pragma warning(disable: 4458) // declaration hides class member
#pragma warning(disable: 4459) // declaration hides global declaration
#endif
// The user_code fragment deliberately omits the headers field in a
// designated initializer; the default is part of the lesson.
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#endif
#if defined(__clang__) && defined(__has_warning)
#if __has_warning("-Wmissing-designated-field-initializers")
#pragma clang diagnostic ignored "-Wmissing-designated-field-initializers"
#endif
#endif
#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(make_buffer(buf));
if (ec)
co_return;
// Process and respond...
std::string response(buf, n);
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
Transport Independence
Type erasure decouples your code from specific transport implementations:
// Your library code
task<> send_message(any_write_stream& stream, message const& msg)
{
co_await write(stream, make_buffer(msg.header));
co_await write(stream, make_buffer(msg.body));
}
Callers provide any conforming implementation:
// TCP socket
tcp::socket socket;
any_write_stream stream{&socket}; // references socket
co_await send_message(stream, msg);
// TLS stream
tls::stream tls;
any_write_stream stream{&tls}; // references tls
co_await send_message(stream, msg);
// Test mock
test::write_stream mock(f);
any_write_stream stream{&mock}; // references mock
co_await send_message(stream, 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
task<> caller(tcp::socket& socket)
{
any_stream stream{&socket}; // Wrap by reference here
co_await 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}; // reference; socket must outlive stream
// or: any_stream stream{std::move(socket)}; // wrapper takes ownership
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_stream.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_stream body; // Body is read as a stream
};
// 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}; // references socket
http_request req{
.method = "GET",
.url = "/api/data"
};
auto response = co_await send_request(conn, req);
// Read body through type-erased source
char storage[4096];
mutable_buffer buf(storage, sizeof(storage));
auto [ec, n] = co_await response.body.read_some(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:
-
Consider batching operations
-
Use concrete types in hot paths
-
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_buffer_source,any_buffer_sink
You have now completed the Stream Concepts section. These abstractions—streams and their type-erased wrappers—form the foundation for Capy’s I/O model. Continue to Example Programs to see complete working examples.