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_result<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.
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_result
async_result<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_result 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_result do not need
to include Asio or other heavyweight dependencies, keeping compile
times low and interfaces clean.
Use make_async_result<T>() to create an async_result from any
callable that follows the deferred operation pattern.
The async_result<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_result
Return async_result<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_result using make_async_result<T>().
async_result<std::size_t> async_read(socket& s, buffer& b)
{
return make_async_result<std::size_t>(
[&](auto handler) {
s.async_read(b, std::move(handler));
});
}
Use async_result 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_result. -
Composing multiple awaitable operations? Use
task. -
Exposing a library function without leaking dependencies? Use
async_resultwith the implementation in a source file.
In practice, application code is primarily task-based, while
async_result 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_result.hpp>
namespace mylib {
// Returns the number of milliseconds actually elapsed
boost::capy::async_result<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_result<int>
async_wait(int milliseconds)
{
return boost::capy::make_async_result<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_result<void> for
operations that complete without producing a value.
#include <boost/capy/task.hpp>
#include <boost/capy/async_result.hpp>
using boost::capy::task;
using boost::capy::async_result;
using boost::capy::make_async_result;
// Wrap a callback-based timer (void result)
async_result<void> async_sleep(int milliseconds)
{
return make_async_result<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;
}
Running a task to completion
Tasks are lazy and require a driver to execute. This example shows a simple synchronous driver that runs a task until it completes.
#include <boost/capy/task.hpp>
using boost::capy::task;
template<class T>
T run(task<T> t)
{
bool done = false;
t.handle().promise().on_done_ = [&done]{ done = true; };
t.handle().resume();
// In a real application, this would integrate with
// an event loop rather than spinning
while (!done)
{
// Process pending I/O events here
}
return t.await_resume();
}
task<int> compute()
{
co_return 42;
}
int main()
{
int result = run(compute());
return result == 42 ? 0 : 1;
}
Complete request handler
This example combines tasks and async_result to implement a request handler that reads a request, processes it, and sends a response.
#include <boost/capy/task.hpp>
#include <boost/capy/async_result.hpp>
#include <string>
using boost::capy::task;
using boost::capy::async_result;
// Forward declarations - implementations use async_result
// to wrap the underlying I/O library
async_result<std::string> async_read(int fd);
async_result<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);
}