The task Type
This section introduces Capy’s task<T> type—the fundamental coroutine type for asynchronous programming in Capy.
Overview
task<T> is Capy’s primary coroutine return type. It represents an asynchronous operation that eventually produces a value of type T (or nothing, for task<void>).
Key characteristics:
-
Lazy execution — The coroutine does not start until awaited
-
Symmetric transfer — Efficient resumption without stack accumulation
-
Executor inheritance — Inherits the caller’s executor unless explicitly bound
-
Stop token propagation — Forward-propagates cancellation signals
-
HALO support — Enables Heap Allocation eLision Optimization when possible
Declaring task Coroutines
Any function that returns task<T> and contains coroutine keywords (co_await, co_return) is a task coroutine:
#include <boost/capy.hpp>
using namespace boost::capy;
task<int> compute_value()
{
co_return 42;
}
task<std::string> fetch_greeting()
{
co_return "Hello, Capy!";
}
task<> do_nothing() // task<void>
{
co_return;
}
The syntax task<> is equivalent to task<void> and represents a coroutine that completes without producing a value.
Returning Values with co_return
Use co_return to complete the coroutine and provide its result:
task<int> add(int a, int b)
{
int result = a + b;
co_return result; // Completes with value
}
task<> log_message(std::string msg)
{
std::cout << msg << "\n";
co_return; // Completes without value
}
For task<void>, you can either use co_return; explicitly or let execution fall off the end of the function body.
Running a Task
A task is lazy, so declaring one does not run it. To run a task to completion from ordinary (non-coroutine) code, pass it to run_async with an executor and a completion handler. The task runs on the executor, and its result is delivered to the handler:
// You have a task; run it on an executor and observe its result.
thread_pool pool(1);
auto ex = pool.get_executor();
int total = 0;
run_async(ex, [&](int result) {
std::cout << "Result: " << result << "\n"; // prints 5
total = result;
})(add(2, 3));
pool.join(); // wait for the pooled task to finish
Here add(2, 3) runs on a thread_pool executor, and the completion handler receives the result, 5. The call to pool.join() waits for the pooled work to finish before the result is read.
Awaiting Other Tasks
Tasks can await other tasks using co_await. This is the primary mechanism for composing asynchronous operations:
task<int> step_one()
{
co_return 10;
}
task<int> step_two(int x)
{
co_return x * 2;
}
task<int> full_operation()
{
int a = co_await step_one(); // Suspends until step_one completes
int b = co_await step_two(a); // Suspends until step_two completes
co_return b + 5; // Final result: 25
}
When you co_await a task:
-
The current coroutine suspends
-
The awaited task starts executing
-
When the awaited task completes, the current coroutine resumes
-
The
co_awaitexpression evaluates to the awaited task’s result
Lazy Execution
A critical property of task<T> is lazy execution: creating a task does not start its execution. The coroutine body runs only when the task is awaited.
task<int> compute()
{
std::cout << "Computing...\n"; // Not printed until awaited
co_return 42;
}
task<> example()
{
auto t = compute(); // Task created, but "Computing..." NOT printed yet
std::cout << "Task created\n";
int result = co_await std::move(t); // NOW "Computing..." is printed
std::cout << "Result: " << result << "\n";
}
Output:
Task created
Computing...
Result: 42
Lazy execution enables efficient composition—tasks that are never awaited never run, consuming no resources beyond their initial allocation.
Symmetric Transfer
When a task completes, control transfers directly to its continuation (the coroutine that awaited it) using symmetric transfer. This avoids stack accumulation even with deep chains of coroutine calls.
Consider:
task<> a() { co_await b(); }
task<> b() { co_await c(); }
task<> c() { co_return; }
Without symmetric transfer, each co_await would add a stack frame, potentially causing stack overflow with deep nesting. With symmetric transfer, c returning to b returning to a uses constant stack space regardless of depth.
This is implemented through the await_suspend returning a coroutine handle rather than void:
// Inside task's final_suspend awaiter
std::coroutine_handle<> await_suspend(std::coroutine_handle<>) const noexcept
{
return continuation_; // Transfer directly to continuation
}
Move Semantics
Tasks are move-only. Copying a task would create aliasing problems where multiple handles reference the same coroutine frame.
task<int> compute();
task<> example()
{
auto t1 = compute();
auto t2 = std::move(t1); // OK: ownership transferred, t1 is now empty
// auto t3 = t2; // Error: task is not copyable
int result = co_await std::move(t2);
}
After moving, the source task becomes empty and must not be awaited.
Exception Propagation
Exceptions thrown inside a task are captured and rethrown when the task is awaited:
task<int> might_fail(bool should_fail)
{
if (should_fail)
throw std::runtime_error("Operation failed");
co_return 42;
}
task<> example()
{
try
{
int result = co_await might_fail(true);
}
catch (std::runtime_error const& e)
{
std::cout << "Caught: " << e.what() << "\n";
}
}
The exception is stored in the promise when it occurs and rethrown in await_resume when the calling coroutine resumes.
The task<T> type is defined in:
// Fragments deliberately leave results and bindings unused; the pages
// explain the values in prose instead.
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-variable"
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wunused-value"
#pragma GCC diagnostic ignored "-Wunused-result"
#pragma GCC diagnostic ignored "-Wunused-function"
// gcc 15 with sanitizers misattributes coroutine frame delete paths
#pragma GCC diagnostic ignored "-Wmismatched-new-delete"
#endif
#if defined(__clang__)
#pragma clang diagnostic ignored "-Wunused-lambda-capture"
#pragma clang diagnostic ignored "-Wunused-private-field"
#endif
#if defined(_MSC_VER)
#pragma warning(disable: 4834) // discarding [[nodiscard]] return value
#pragma warning(disable: 4189) // local variable initialized but not referenced
#pragma warning(disable: 4100) // unreferenced formal parameter
#pragma warning(disable: 4101) // unreferenced local variable
#pragma warning(disable: 4456) // declaration hides previous local declaration
#pragma warning(disable: 4457) // declaration hides function parameter
#pragma warning(disable: 4458) // declaration hides class member
#pragma warning(disable: 4459) // declaration hides global declaration
#endif
#include <boost/capy/task.hpp>
Or included via the umbrella header:
#include <boost/capy.hpp>