Streams (Partial I/O)
ReadStream
A type satisfies ReadStream if it provides partial read operations via read_some:
template<typename T>
concept ReadStream =
requires(T& stream, mutable_buffer_archetype buffers)
{
{ stream.read_some(buffers) } -> IoAwaitable;
requires awaitable_decomposes_to<
decltype(stream.read_some(buffers)),
std::error_code, std::size_t>;
};
The requires clause names a single representative buffer (mutable_buffer_archetype) because a C++ concept cannot say "works with every buffer sequence." The real contract is that read_some accepts any MutableBufferSequence—one buffer or a range; the archetype only samples that requirement.
read_some Semantics
See ReadStream for the full contract: return-value semantics, error reporting, throws, and buffer lifetime.
Partial Transfer
read_some may return fewer bytes than the buffer can hold:
char buf[1024];
auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf));
// n might be 1, might be 500, might be 1024
// if !ec, then n >= 1
This matches underlying OS behavior: reads return when some data is available.
WriteStream
A type satisfies WriteStream if it provides partial write operations via write_some:
template<typename T>
concept WriteStream =
requires(T& stream, const_buffer_archetype buffers)
{
{ stream.write_some(buffers) } -> IoAwaitable;
requires awaitable_decomposes_to<
decltype(stream.write_some(buffers)),
std::error_code, std::size_t>;
};
As with ReadStream, the const_buffer_archetype is only a representative: the real contract is that write_some accepts any ConstBufferSequence, which a C++ concept cannot fully express.
write_some Semantics
See WriteStream for the full contract: return-value semantics, error reporting, throws, and buffer lifetime.
Type-Erasing Wrappers
any_read_stream
Wraps any ReadStream in a type-erased container:
#include <boost/capy/io/any_read_stream.hpp>
// Owning: takes ownership of a moved-in stream
template<capy::ReadStream S>
any_read_stream(S stream);
// Reference: wraps by pointer without ownership
template<capy::ReadStream S>
any_read_stream(S* stream);
any_write_stream
Wraps any WriteStream:
#include <boost/capy/io/any_write_stream.hpp>
template<capy::WriteStream S>
any_write_stream(S stream); // owning
template<capy::WriteStream S>
any_write_stream(S* stream); // reference
any_stream
Wraps bidirectional streams (both ReadStream and WriteStream):
#include <boost/capy/io/any_stream.hpp>
template<class S>
requires capy::ReadStream<S> && capy::WriteStream<S>
any_stream(S stream); // owning
template<class S>
requires capy::ReadStream<S> && capy::WriteStream<S>
any_stream(S* stream); // reference
Wrapper Characteristics
All wrappers share these properties:
-
Owning or reference: By-value construction owns a moved-in object; pointer construction wraps by reference
-
Preallocated coroutine frame: Zero steady-state allocation
-
Move-only: Non-copyable; moving transfers the cached frame
-
Lifetime requirement: A pointer-wrapped object must outlive the wrapper
Example usage:
void process_stream(capy::any_stream& stream);
auto [client, server] = capy::test::make_stream_pair();
// Type erasure, references the existing stream
capy::any_stream wrapped{&client};
// process_stream doesn't know about test::stream
process_stream(wrapped);
Example: Echo Server with any_stream
// echo.hpp - Header only declares the signature
capy::task<> handle_connection(capy::any_stream& stream);
// echo.cpp - Implementation in separate translation unit
capy::task<> handle_connection(capy::any_stream& stream)
{
char buf[1024];
for (;;)
{
auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf));
auto [wec, wn] = co_await capy::write(
stream, capy::const_buffer(buf, n));
if (ec)
break;
if (wec)
break;
}
}
The implementation doesn’t know the concrete stream type. It compiles once and works with any transport.