The results Type

I/O operations in Capy report an expected failure, such as reaching the end of a stream, as a value. Exceptions are kept for genuine errors, such as a failed frame allocation. The value is a results<Ts…​>, declared in:

#include <boost/capy/results.hpp>

results<Ts…​> is an alias for std::tuple<std::error_code, Ts…​>. The error code always comes first. Zero or more payload values follow it. Stream operations return results<std::size_t>: an error code, then a byte count.

Consuming results

Unpack the result with a structured binding, then test the error code before using the payload:

capy::task<std::size_t> read_once(capy::test::read_stream& rs)
{
    char buf[64];
    // read_some returns results<std::size_t>: an error code, then a byte count
    auto [ec, n] = co_await rs.read_some(capy::make_buffer(buf));
    if(ec)          // always test the error code first
        co_return 0;
    co_return n;    // n is meaningful because ec is clear
}

Compare the error code against a condition, not an exact error value. For example, cond::eof matches the end-of-stream error that Capy’s streams and Corosio’s sockets report.

Payload Alongside an Error

Whether a payload is meaningful when the error code is set depends on the operation that produced it. Many I/O operations report a meaningful partial result alongside the error. read and write report the bytes transferred before the condition stopped them:

capy::task<> read_header(capy::test::read_stream& rs)
{
    char buf[8];
    // read keeps going until buf is full, or a condition stops it
    auto [ec, n] = co_await capy::read(rs, capy::make_buffer(buf));
    if(ec == capy::cond::eof)
    {
        // The stream ended early. n still counts the bytes
        // placed in buf before it did.
        std::string_view got(buf, n);
        (void)got;
    }
}

Given a stream that holds only "hi", this read stops with cond::eof and n == 2. Other operations leave the payload unspecified on error, so check each operation’s reference page.

Reusing Variables with std::tie

Because results is a std::tuple, std::tie assigns into variables that already exist. This suits a loop, where a structured binding would introduce new names on every pass:

capy::task<std::string> read_all(capy::test::read_stream& rs)
{
    std::string body;
    char buf[64];
    std::error_code ec;
    std::size_t n = 0;
    for(;;)
    {
        // std::tie assigns into ec and n; no new bindings per iteration
        std::tie(ec, n) = co_await rs.read_some(capy::make_buffer(buf));
        if(ec)
            break;
        body.append(buf, n);
    }
    // ec == capy::cond::eof when the stream ended normally
    co_return body;
}

The rest of the standard tuple API applies too: std::get, std::apply, std::tuple_cat, comparisons, and tuple assignment.

Producing results

A coroutine that reports errors this way returns task<results<Ts…​>>:

// `results<Ts...>` holds an error code `ec` plus zero or more payload
// values. A task returning one reports failure without throwing.

capy::task<capy::results<>> ensure_ready(bool ready)
{
    if(! ready)
        co_return std::make_error_code(std::errc::not_connected);  // ec converts
    co_return {};                                             // success
}

capy::task<capy::results<std::size_t>> count_ready(bool ready)
{
    using result = capy::results<std::size_t>;
    if(! ready)
        co_return result{std::make_error_code(std::errc::not_connected), 0};
    co_return result{std::error_code(), 42};  // success, carrying a value
}

capy::task<> use_them()
{
    // `results` models the tuple protocol: ec first, then the payloads.
    auto [ec, n] = co_await count_ready(true);
    if(ec)
        co_return;  // always check ec first
    (void)n;        // n is only meaningful when ec is falsy
}

For results<>, with no payload, a std::error_code converts implicitly, so co_return some_ec; compiles. With payloads present you must supply the whole tuple, as count_ready shows.

Where results Appears

  • Streams — read_some, write_some, read and write return results<std::size_t>.

  • Concurrent Composition — when_all flattens its children’s payloads into one results. when_any returns a std::variant whose first alternative is the error code and whose others are the children’s payloads.

  • Async Mutex — async_mutex's scoped_lock() returns an error code and a lock guard.

  • Contingencies — What a contingency is, and how ec signals one.