The task Type

This section introduces Capy’s task<T> type—the fundamental coroutine type for asynchronous programming in Capy.

Prerequisites

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:

  1. The current coroutine suspends

  2. The awaited task starts executing

  3. When the awaited task completes, the current coroutine resumes

  4. The co_await expression 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.