乐于分享
好东西不私藏

通用 C++20 协程任务类:一个模板化的实现

通用 C++20 协程任务类:一个模板化的实现
在上一篇文章中,我们为协程函数单独定义了一个返回值结构体,其中必须内嵌 promise_type 并实现若干接口。然而,当项目中出现多个返回值类型不同(int、void、double 等)的协程时,为每一种返回值重复编写几乎相同的结构体会造成大量冗余代码。
为了提升代码复用性,我们希望设计一个通用的协程任务类,通过模板参数来指定协程的返回值类型。同时,还需要处理”有返回值”与“无返回值”两种情况的差异:
  • 对于有返回值类型 T,promise_type 中需要定义 return_value(T) 和 yield_value(T),并存储一个 T 类型的 result 成员。
  • 对于 void,需要定义 return_void(),且不存储结果。

1. 设计思路

我们可以采用以下分层设计:
  1. 基类 _promise_type_base:存放所有 promise_type 共有的函数(initial_suspend、final_suspend、unhandled_exception)以及异常存储。
  2. 派生模板类 _promise_type:处理非 void 的返回值类型,提供 return_value、yield_value 和 result 成员。
  3. 特化 _promise_type:处理无返回值情况,提供 return_void()。
  4. 通用任务类 CoroTask:对外暴露统一的接口,内部根据 promise_type 构建句柄,并封装恢复执行、获取结果等操作。

2. 完整代码实现

下面给出一个完整的通用协程任务类实现,支持任意返回值类型(包括 void)。
#include<coroutine>#include<stdexcept>// 前置声明template <typename T>struct CoroTask;// ========== promise_type 基类(公共部分) ==========template <typename T>struct _promise_type_base{    // 协程初始挂起策略(可根据需要改为 suspend_never)    autoinitial_suspend()noexcept{        return std::suspend_always{};    }    // 协程最终挂起策略    autofinal_suspend()noexcept{        return std::suspend_always{};    }    // 异常处理:保存异常指针    voidunhandled_exception()noexcept{        except = std::current_exception();    }    std::exception_ptr except;};// ========== 有返回值版本的 promise_type ==========template <typename T>struct _promise_type : public _promise_type_base<T>{    // 创建协程返回值对象    CoroTask<T> get_return_object(){        return CoroTask<T>{std::coroutine_handle<_promise_type<T>>::from_promise(*this)};    }    // 处理 co_yield 表达式(左值和右值重载)    autoyield_value(T&& value){        this->result = std::forward<T>(value);        return std::suspend_always{};    }    autoyield_value(const T& value){        this->result = value;        return std::suspend_always{};    }    // 处理 co_return expr(有返回值)    voidreturn_value(T&& value)noexcept{        result = std::forward<T>(value);    }    voidreturn_value(const T& value)noexcept{        result = value;    }    T result;   // 存储协程的返回值};// ========== 无返回值特化 (void) ==========template <>struct _promise_type<void> : public _promise_type_base<void>{    CoroTask<voidget_return_object();   // 定义见下方    voidreturn_void()noexcept{}        // 对应 co_return;};// ========== 通用协程任务类 ==========template<typename T>struct CoroTask{    using promise_type = _promise_type<T>;    // 构造函数    explicitCoroTask(std::coroutine_handle<promise_type> h)        : handle(h)    {}    // 禁用拷贝,支持移动    CoroTask(const CoroTask&) = delete;    CoroTask& operator=(const CoroTask&) = delete;    CoroTask(CoroTask&& other) noexcept        : handle(other.handle)    {        other.handle = nullptr;    }    CoroTask& operator=(CoroTask&& other) noexcept    {        if (this != &other) {            if (handle) handle.destroy();            handle = other.handle;            other.handle = nullptr;        }        return *this;    }    ~CoroTask()    {        if (handle) handle.destroy();    }    // 恢复协程执行    voidresume()    {        if (!handle.done()) {            handle.resume();        }    }    // 获取结果(仅当 T 不是 void 时可用)    result()requires(!std::is_same_v<T, void>)    {        if (!handle.done()) {            throw std::runtime_error("Coroutine not completed");        }        if (handle.promise().except) {            std::rethrow_exception(handle.promise().except);        }        return handle.promise().result;    }    std::coroutine_handle<promise_type> handle;};// 实现 void 特化的 get_return_object(因为此时 CoroTask<void> 尚未完整定义,放在类外)inline CoroTask<void> _promise_type<void>::get_return_object(){    return CoroTask<void>{        std::coroutine_handle<_promise_type<void>>::from_promise(*this)    };}

3. 关键设计说明

3.1 基类的作用

_promise_type_base 集中定义了所有协程共有的行为:
  • initial_suspend():返回 std::suspend_always,协程创建后立即挂起,需要外部调用 resume() 才能启动。
  • final_suspend():返回 std::suspend_always,协程结束后仍然保持挂起状态,以便外部在销毁前仍能访问结果或异常。
  • unhandled_exception():捕获未处理的异常,保存到 std::exception_ptr 中,供后续 result() 函数重新抛出。

3.2 有返回值与无返回值的差异

  • 非 void 版本:定义了 return_value(处理 co_return expr)和 yield_value(处理 co_yield expr),并拥有 T result 成员。
  • void 特化:只定义 return_void(),不存储结果。yield_value 对于 void 没有意义,因此未提供(若在协程中使用 co_yield 会导致编译错误)。

3.3 任务类的接口

  • 构造与移动语义:禁止拷贝,允许移动,确保句柄的唯一所有权。
  • resume():恢复协程执行,如果协程已经结束则不做任何操作。
  • result():仅当 T 不是 void 时可用(使用 C++20 的 requires 子句)。它会先检查协程是否已完成,若未完成则抛出异常;否则检查是否有保存的异常并重新抛出,最后返回结果。
  • 析构:自动销毁协程句柄,释放资源。

4. 使用示例

下面展示了如何使用这个通用 CoroTask 类来编写具有不同返回值的协程函数。
#include<iostream>#include<thread>#include<chrono>// 协程 1:返回 intCoroTask<intcoroutineFunc1(int value){    std::cout << "coroutineFunc1 start.\n";    std::this_thread::sleep_for(std::chrono::seconds(2));    int result = value * 10;    co_return result;   // 调用 return_value(int)}// 协程 2:无返回值CoroTask<voidcoroutineFunc2(){    std::cout << "coroutineFunc2 start.\n";    std::this_thread::sleep_for(std::chrono::seconds(2));    co_return;          // 调用 return_void()}intmain(){    // 创建两个协程(此时都处于初始挂起状态)    auto task1 = coroutineFunc1(3);    auto task2 = coroutineFunc2();    std::cout << "main tasks ...\n";    // 恢复协程执行    task1.resume();    task2.resume();    // 获取协程 1 的结果(协程 2 无结果)    std::cout << "coroutineFunc1 result: " << task1.result() << "\n";    return 0;}
输出结果:
main tasks ...coroutineFunc1 start.coroutineFunc2 start.coroutineFunc1 result: 30

5. 总结

通过模板和特化,我们实现了一个通用的协程任务类 CoroTask,它能够:
  • 自动适配有返回值(T)和无返回值(void)两种协程。
  • 统一处理协程的创建、挂起、恢复、结果获取和异常传播。
  • 避免了为每个协程重复编写 promise_type 结构体的冗余工作。
这个通用类可以作为 C++20 协程开发的基础设施,大大简化异步任务、生成器等模式的实现。读者可以根据实际需求调整挂起策略(如将 initial_suspend 改为 suspend_never 以实现立即执行),或扩展支持 co_yield 的迭代器接口。