Composed Operations

Corosio provides composed operations that build on the primitive read_some() and write_some() functions to provide higher-level guarantees.

Code snippets assume:

#include <boost/capy/read.hpp>
#include <boost/capy/write.hpp>
#include <boost/capy/buffers/string_dynamic_buffer.hpp>
#include <boost/capy/buffers.hpp>

namespace corosio = boost::corosio;
namespace capy = boost::capy;

The Problem with Primitives

The primitive operations read_some() and write_some() provide no guarantees about how much data is transferred:

char buf[1024];
auto [ec, n] = co_await s.read_some(
    capy::mutable_buffer(buf, sizeof(buf)));
// n could be 1, 100, 500, or 1024 - no guarantee

For many use cases, you need to transfer a specific amount of data. Composed operations provide these guarantees.

capy::read()

The read() function reads until the buffer is full or an error occurs:

char buf[1024];
auto [ec, n] = co_await capy::read(
    stream, capy::mutable_buffer(buf, sizeof(buf)));

// Either:
// - n == 1024 and ec is default (success)
// - ec == capy::cond::eof and n < 1024 (reached end of stream)
// - ec is some other error

Signature

auto
read(
    ReadStream auto& stream,
    MutableBufferSequence auto const& buffers) ->
        capy::io_task<std::size_t>;

Behavior

  1. Calls read_some() repeatedly until all buffers are filled

  2. If read_some() returns an error (including cond::eof), returns immediately with bytes read so far

  3. On success, returns total bytes (equals buffer_size(buffers))

capy::read() into a dynamic buffer

A second overload reads until EOF, growing the buffer as needed. Use capy::string_dynamic_buffer to wrap a std::string:

std::string content;
auto [ec, n] = co_await capy::read(
    stream, capy::string_dynamic_buffer(&content));

// On success (EOF reached): ec is default, n is total bytes read
// On error: ec is the error, n is bytes read before the error

Signature

auto
read(
    ReadStream auto& stream,
    DynamicBufferParam auto&& buffers,
    std::size_t initial_amount = 2048) ->
        capy::io_task<std::size_t>;

Behavior

  1. Prepares initial_amount bytes via buffers.prepare(), then reads

  2. Grows 1.5x when the prepared buffer fills completely

  3. On cond::eof: returns success with total bytes read

  4. On any other error: returns immediately with bytes read so far

capy::write()

The write() function writes all data or fails:

std::string msg = "Hello, World!";
auto [ec, n] = co_await capy::write(
    stream, capy::const_buffer(msg.data(), msg.size()));

// Either:
// - n == msg.size() and ec is default (all data written)
// - ec is an error

Signature

auto
write(
    WriteStream auto& stream,
    ConstBufferSequence auto const& buffers) ->
        capy::io_task<std::size_t>;

Behavior

  1. Calls write_some() repeatedly until all buffers are written

  2. If an error occurs, returns immediately with bytes written so far

  3. On success, returns total bytes (equals buffer_size(buffers))

consuming_buffers Helper

Both read() and write() use consuming_buffers internally to track progress through a buffer sequence:

#include <boost/capy/buffers/consuming_buffers.hpp>

std::array<capy::mutable_buffer, 2> bufs = {
    capy::mutable_buffer(header, 16),
    capy::mutable_buffer(body, 1024)
};

capy::consuming_buffers<decltype(bufs)> consuming(bufs);

// After reading 20 bytes:
consuming.consume(20);
// Now consuming represents: 4 bytes of header remaining + full body

Interface

template<class BufferSequence>
class consuming_buffers
{
public:
    explicit consuming_buffers(BufferSequence const& bufs);

    void consume(std::size_t n);

    const_iterator begin() const;
    const_iterator end() const;
};

The iterator returns adjusted buffers accounting for consumed bytes.

Error Handling Patterns

Structured Bindings with EOF Check

auto [ec, n] = co_await capy::read(stream, buf);
if (ec)
{
    if (ec == capy::cond::eof)
        std::cout << "End of stream, read " << n << " bytes\n";
    else
        std::cerr << "Error: " << ec.message() << "\n";
}

Exception Pattern

// For write (EOF doesn't apply)
auto [wec, n] = co_await capy::write(stream, buf);
if (wec)
    capy::detail::throw_system_error(wec);

// For read (need to handle EOF)
auto [rec, rn] = co_await capy::read(stream, buf);
if (rec && rec != capy::cond::eof)
    capy::detail::throw_system_error(rec);

Cancellation

Composed operations support cancellation through the affine protocol. When cancelled, they return with cond::canceled and the partial byte count.

auto [ec, n] = co_await capy::read(stream, large_buffer);
if (ec == capy::cond::canceled)
    std::cout << "Cancelled after reading " << n << " bytes\n";

Performance Considerations

Single vs. Multiple Buffers

For optimal performance with multiple buffers:

// Efficient: single system call per read_some()
std::array<capy::mutable_buffer, 2> bufs = {...};
co_await capy::read(stream, bufs);

// Less efficient: may require more system calls
co_await capy::read(stream, buf1);
co_await capy::read(stream, buf2);

Buffer Sizing

Choose buffer sizes that match your expected data:

  • Too small: More system calls

  • Too large: Memory waste

For unknown-length data (like HTTP responses), use the dynamic buffer overload:

std::string response;
co_await capy::read(stream, capy::string_dynamic_buffer(&response));

Example: HTTP Response Reading

capy::task<std::string> read_http_response(corosio::io_stream& stream)
{
    std::string response;
    auto [ec, n] = co_await capy::read(
        stream, capy::string_dynamic_buffer(&response));

    if (ec)
        capy::detail::throw_system_error(ec);

    co_return response;
}

Next Steps