The task Type
This section introduces Capy’s task<T> type—the fundamental coroutine type for asynchronous programming in Capy.
Prerequisites
-
Completed C++20 Coroutines Tutorial
-
Understanding of promise types, coroutine handles, and symmetric transfer
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 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.
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 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
coro await_suspend(coro) 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
// auto t3 = t2; // Error: task is not copyable
int result = co_await t2; // t1 is now empty
}
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.
Reference
The task<T> type is defined in:
#include <boost/capy/task.hpp>
Or included via the umbrella header:
#include <boost/capy.hpp>
You have now learned how to declare, return values from, and await task<T> coroutines. In the next section, you will learn how to launch tasks for execution using run_async and run.