Coroutines

Introduction

Capy provides lightweight coroutine support for C++20, enabling asynchronous code that reads like synchronous code. The library offers two awaitable types: task<T> for lazy coroutine-based operations, and async_op<T> for bridging callback-based APIs into the coroutine world.

This section covers the awaitable types provided by the library, demonstrates their usage patterns, and presents practical examples showing how to integrate coroutines into your applications.

Coroutine features are only available when compiling with C++20 or later.

Awaitables

task

task<T> is a lazy coroutine type that produces a value of type T. The coroutine does not begin execution when created; it remains suspended until awaited. This lazy evaluation enables structured concurrency where parent coroutines naturally await their children.

A task owns its coroutine handle and destroys it automatically. Exceptions thrown within the coroutine are captured and rethrown when the result is retrieved via co_await.

Tasks support scheduler affinity through the on() method, which binds the task to an executor. When a task has affinity, all internal co_await expressions resume on the specified executor, ensuring consistent execution context.

The task<void> specialization is used for coroutines that perform work but do not produce a value. These coroutines use co_return; with no argument.

async_op

async_op<T> bridges traditional callback-based asynchronous APIs with coroutines. It wraps a deferred operation—a callable that accepts a completion handler, starts an asynchronous operation, and invokes the handler with the result.

The key advantage of async_op is its type-erased design. The implementation details are hidden behind an abstract interface, allowing runtime-specific code such as Boost.Asio to be confined to source files. Headers that return async_op do not need to include Asio or other heavyweight dependencies, keeping compile times low and interfaces clean.

Use make_async_op<T>() to create an async_op from any callable that follows the deferred operation pattern.

The async_op<void> specialization is used for operations that signal completion without producing a value, such as timers, write operations, or connection establishment. The completion handler takes no arguments.

Usage

When to use task

Return task<T> from a coroutine function—one that uses co_await or co_return. The function body contains coroutine logic and the return type tells the compiler to generate the appropriate coroutine machinery.

task<int> compute()
{
    int a = co_await step_one();
    int b = co_await step_two(a);
    co_return a + b;
}

Use task when composing asynchronous operations purely within the coroutine world. Tasks can await other tasks, forming a tree of dependent operations.

When to use async_op

Return async_op<T> from a regular (non-coroutine) function that wraps an existing callback-based API. The function does not use co_await or co_return; instead it constructs and returns an async_op using make_async_op<T>().

async_op<std::size_t> async_read(socket& s, buffer& b)
{
    return make_async_op<std::size_t>(
        [&](auto handler) {
            s.async_read(b, std::move(handler));
        });
}

Use async_op at the boundary between callback-based code and coroutines. It serves as an adapter that lets coroutines co_await operations implemented with traditional completion handlers.

Choosing between them

  • Writing new asynchronous logic? Use task.

  • Wrapping an existing callback API? Use async_op.

  • Composing multiple awaitable operations? Use task.

  • Exposing a library function without leaking dependencies? Use async_op with the implementation in a source file.

In practice, application code is primarily task-based, while async_op appears at integration points with I/O libraries and other callback-driven systems.

Examples

Chaining tasks

This example demonstrates composing multiple tasks into a pipeline. Each step awaits the previous one, and the final result propagates back to the caller.

#include <boost/capy/task.hpp>
#include <string>

using boost::capy::task;

task<int> parse_header(std::string const& data)
{
    // Extract content length from header
    auto pos = data.find("Content-Length: ");
    if (pos == std::string::npos)
        co_return 0;
    co_return std::stoi(data.substr(pos + 16));
}

task<std::string> fetch_data()
{
    // Simulated network response
    co_return std::string("Content-Length: 42\r\n\r\nHello");
}

task<int> get_content_length()
{
    std::string response = co_await fetch_data();
    int length = co_await parse_header(response);
    co_return length;
}

Wrapping a callback API

This example shows how to wrap a hypothetical callback-based timer into an awaitable. The implementation details stay in the source file.

// timer.hpp - public header, no Asio includes
#ifndef TIMER_HPP
#define TIMER_HPP

#include <boost/capy/async_op.hpp>

namespace mylib {

// Returns the number of milliseconds actually elapsed
boost::capy::async_op<int>
async_wait(int milliseconds);

} // namespace mylib

#endif
// timer.cpp - implementation, Asio details hidden here
#include "timer.hpp"
#include <boost/asio.hpp>

namespace mylib {

boost::capy::async_op<int>
async_wait(int milliseconds)
{
    return boost::capy::make_async_op<int>(
        [milliseconds](auto handler)
        {
            // In a real implementation, this would use
            // a shared io_context and steady_timer
            auto timer = std::make_shared<boost::asio::steady_timer>(
                get_io_context(),
                std::chrono::milliseconds(milliseconds));

            timer->async_wait(
                [timer, milliseconds, h = std::move(handler)]
                (boost::system::error_code) mutable
                {
                    h(milliseconds);
                });
        });
}

} // namespace mylib

Void operations

This example shows task<void> and async_op<void> for operations that complete without producing a value.

#include <boost/capy/task.hpp>
#include <boost/capy/async_op.hpp>

using boost::capy::task;
using boost::capy::async_op;
using boost::capy::make_async_op;

// Wrap a callback-based timer (void result)
async_op<void> async_sleep(int milliseconds)
{
    return make_async_op<void>(
        [milliseconds](auto on_done)
        {
            // In real code, this would start a timer
            // and call on_done() when it expires
            start_timer(milliseconds, std::move(on_done));
        });
}

// A void task that performs work without returning a value
task<void> log_with_delay(std::string message)
{
    co_await async_sleep(100);
    std::cout << message << std::endl;
    co_return;
}

task<void> run_sequence()
{
    co_await log_with_delay("Step 1");
    co_await log_with_delay("Step 2");
    co_await log_with_delay("Step 3");
    co_return;
}

Spawning tasks on an executor

Tasks are lazy and require a driver to execute. The spawn() function starts a task on an executor and delivers the result to a completion handler. This is useful for launching tasks from non-coroutine code or integrating tasks into callback-based systems.

#include <boost/capy/task.hpp>
#include <boost/capy/executor.hpp>

using boost::capy::task;
using boost::capy::executor;
using boost::capy::spawn;

task<int> compute()
{
    co_return 42;
}

void start_computation(executor ex)
{
    // Spawn a task on the executor with a completion handler
    spawn(ex, compute(), [](auto result) {
        if (result.has_value())
            std::cout << "Result: " << *result << std::endl;
        else
            std::cerr << "Error occurred\n";
    });
}

The spawn() function takes an executor, a task, and a completion handler. The handler receives system::result<T, std::exception_ptr> which holds either the task’s return value or any exception thrown during execution. The task runs to completion on the executor with proper scheduler affinity.

Complete request handler

This example combines tasks and async_op to implement a request handler that reads a request, processes it, and sends a response.

#include <boost/capy/task.hpp>
#include <boost/capy/async_op.hpp>
#include <string>

using boost::capy::task;
using boost::capy::async_op;

// Forward declarations - implementations use async_op
// to wrap the underlying I/O library
async_op<std::string> async_read(int fd);
async_op<std::size_t> async_write(int fd, std::string data);

// Pure coroutine logic using task
task<std::string> process_request(std::string const& request)
{
    // Transform the request into a response
    co_return "HTTP/1.1 200 OK\r\n\r\nHello, " + request;
}

task<int> handle_connection(int fd)
{
    // Read the incoming request
    std::string request = co_await async_read(fd);

    // Process it
    std::string response = co_await process_request(request);

    // Send the response
    std::size_t bytes_written = co_await async_write(fd, response);

    co_return static_cast<int>(bytes_written);
}