如何在一个可移动的协程调用者上调用函数,同时不使用可等待对象?
I have an object Caller that creates a coroutine. The coroutine needs to
call member functions on Caller, so it has to keep some kind of
reference/pointer to it.
我有一个对象 Caller,它会创建一个协程。这个协程需要对 Caller 的成员函数进行调用,因此必须保留某种引用/指针指向它。
The problem: Caller is movable (e.g. it lives in a container that may
reallocate, or it gets returned by value). If Caller moves, any raw
pointer/reference the coroutine frame stored becomes dangling.
问题在于:Caller 是可移动的(例如它驻留在可能重新分配的容器中,或是以值返回)。如果 Caller 移动,协程帧中存储的任何原始指针/引用都会变成悬空。
struct Caller {
void greet();
};
Task coro(Caller& c) { // ref copied into the frame — dangles on move
while (true) { c.greet(); co_await std::suspend_always{}; }
}
Constraints I want to satisfy:
约束条件我想要满足:
- No heap allocation.
-
不进行堆分配。
-
Calleris movable. -
Caller可移动。 -
The coroutine must observe the current address of
Callereven after
the latter is moved. -
协程必须在后续执行时看到
Caller的“当前”地址,即使它已经被移动。
Is there a way to design a parameter type Param such that the reference-pointer
always remains valid through the life of the coroutine?
有没有办法设计一个参数类型 Param,使得引用指针在协程的整个生命周期内始终保持有效?
解决方案
I call this the cross-pointer idiom. Use two pointers that point at each other:
我把这称为跨指针(cross-pointer)用法。使用两个彼此指向对方的指针:
- The
Paramthat lives in the coroutine frame holdsCaller* caller. -
驻留在协程帧中的
Param保存着Caller* caller。 -
CallerholdsParam* live— the address of theParaminside the
coroutine frame. -
Caller保存着Param* live— 也就是协程帧内的Param的地址。
Each side patches the other when it moves:
当任一方移动时,双方互相修补:
-
Param's copy/move constructor writescaller->live = this. This is what
makes the link survive the implicit parameter copy the coroutine performs
when building its frame: the temporary you pass in registers itself, then
the frame-resident copy immediately overwrites the registration with its
own (stable) address. -
Param的拷贝/移动构造函数会写入caller->live = this。这正是让链接在协程在构建帧时执行的隐式参数拷贝过程中仍然有效的原因:你传入的临时对象会先进行自我注册,随后帧内的拷贝会立即用它自己的(稳定的)地址覆盖该注册。 -
Caller's move constructor writeslive->caller = this, so the coroutine
sees the new address on its next resume. -
Caller的移动构造函数写入live->caller = this,因此协程在下一次恢复时就会看到新的地址。 -
Both destructors null the other side so a dead party is observable.
-
两个析构函数都会将对方置为空指针,这样就能在对方已经不可用时被观察到。
struct Caller;
struct Param {
Caller* caller;
explicit Param(Caller* c);
Param(const Param& o);
Param(Param&& o) noexcept;
~Param();
Param& operator=(const Param&) = delete;
Param& operator=(Param&&) = delete;
};
struct Caller {
Param* live = nullptr;
Caller() = default;
Caller(Caller&& o) noexcept : live(o.live) {
if (live) live->caller = this;
o.live = nullptr;
}
~Caller() { if (live) live->caller = nullptr; }
};
Param::Param(Caller* c) : caller(c) { if (caller) caller->live = this; }
Param::Param(const Param& o) : caller(o.caller) { if (caller) caller->live = this; }
Param::Param(Param&& o) noexcept : caller(o.caller) {
if (caller) caller->live = this;
o.caller = nullptr;
}
Param::~Param() { if (caller && caller->live == this) caller->live = nullptr; }
The coroutine just reads p.caller on every resume:
协程在每次恢复时只需要读取 p.caller。
Task coro(Param p) {
while (p.caller) { p.caller->greet(); co_await std::suspend_always{}; }
}
Here is a working example:
下面是一个可工作的示例: