如何在不使用awaitable的情况下调用协程可移动Caller的函数并避免悬空?
要解决可移动Caller对象移动后协程指针悬空的问题,同时满足无堆分配、协程始终能访问Caller当前实例的约束,以下是两种可行方案:
方案一:协程恢复时主动获取Caller当前地址
核心思路是让协程不存储任何指向Caller的指针/引用,而是在每次恢复执行时,由Caller主动传递自身的当前地址,从根源上避免悬空指针问题。
代码实现
#include <coroutine> // 自定义协程任务类型,不存储Caller指针 struct Task { struct promise_type { Task get_return_object() { return {}; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} // 自定义栈分配(无堆内存),需根据协程实际栈使用调整大小 static void* allocate_frame(std::size_t size) { static thread_local char stack[1024]; if (size > sizeof(stack)) throw std::bad_alloc(); return stack; } static void deallocate_frame(void* ptr) {} }; }; // 等待器:用于在协程恢复时接收Caller的当前地址 struct AwaitCaller { Caller* caller = nullptr; bool await_ready() const noexcept { return false; } void await_suspend(std::coroutine_handle<>) const noexcept {} Caller* await_resume() const noexcept { return caller; } }; struct Caller { void greet() { /* 实现你的业务逻辑 */ } // 创建协程,协程不持有Caller的引用/指针 Task create_coro() { return coro_impl(); } // 恢复协程并传递当前Caller地址 void resume_coro(std::coroutine_handle<Task::promise_type> handle) { // 通过等待器传递当前Caller地址 auto& awaiter = *static_cast<AwaitCaller*>(handle.promise().awaiter_ptr); awaiter.caller = this; handle.resume(); } }; // 协程实现:每次挂起后恢复时获取最新的Caller地址 Task coro_impl() { while (true) { AwaitCaller awaiter; // 挂起等待Caller恢复并传递地址 Caller* c = co_await awaiter; c->greet(); // 再次挂起 co_await std::suspend_always{}; } }
方案说明
- 无堆分配:通过自定义
promise_type的内存分配逻辑,使用线程局部栈存储协程帧,完全避免堆内存申请。 - Caller可移动:协程不存储任何
Caller的引用或指针,每次恢复时由Caller传递最新地址,移动后不会出现悬空。 - 始终访问有效实例:每次协程执行时,拿到的都是
Caller的当前地址,确保成员函数调用安全。
方案二:Caller维护协程列表,移动时更新指针
如果需要协程主动访问Caller,可以让Caller维护所有它创建的协程列表,在移动时遍历列表更新协程中的Caller指针。
代码实现
#include <coroutine> struct Task { Caller* caller = nullptr; Task* next = nullptr; struct promise_type { Task get_return_object() { return {}; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} // 栈分配协程帧 static void* allocate_frame(std::size_t size) { static thread_local char stack[1024]; if (size > sizeof(stack)) throw std::bad_alloc(); return stack; } static void deallocate_frame(void* ptr) {} }; }; struct Caller { Task* first_coro = nullptr; void greet() { /* 实现你的业务逻辑 */ } // 移动构造:更新所有关联协程的Caller指针 Caller(Caller&& other) noexcept { for (Task* coro = other.first_coro; coro; coro = coro->next) { coro->caller = this; } first_coro = other.first_coro; other.first_coro = nullptr; } // 移动赋值:清理旧协程指针并更新新协程指针 Caller& operator=(Caller&& other) noexcept { if (this != &other) { // 标记旧协程的Caller指针为无效 for (Task* coro = first_coro; coro; coro = coro->next) { coro->caller = nullptr; } // 更新新协程的Caller指针为当前实例 for (Task* coro = other.first_coro; coro; coro = coro->next) { coro->caller = this; } first_coro = other.first_coro; other.first_coro = nullptr; } return *this; } // 创建协程并加入列表 Task create_coro() { Task coro; coro.caller = this; coro.next = first_coro; first_coro = &coro; return coro; } }; // 协程实现:使用Task中维护的caller指针访问Caller Task coro(Task& t) { while (true) { if (t.caller) { t.caller->greet(); } co_await std::suspend_always{}; } }
方案说明
- 无堆分配:协程帧使用栈存储,
Task对象可栈分配或作为Caller成员,无需堆内存。 - Caller可移动:移动时遍历协程列表,将所有协程的
caller指针更新为当前实例地址,避免悬空。 - 协程自动访问有效实例:协程通过
Task中维护的指针访问Caller,始终指向最新的实例地址。
内容的提问来源于stack exchange,提问作者Arjonais
相关产品推荐
相关产品推荐

