如何在一个可移动的协程调用者上调用函数,同时不使用可等待对象?

编程语言 2026-07-09

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.
  • 不进行堆分配。

  • Caller is movable.

  • Caller 可移动。

  • The coroutine must observe the current address of Caller even 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 Param that lives in the coroutine frame holds Caller* caller.
  • 驻留在协程帧中的 Param 保存着 Caller* caller

  • Caller holds Param* live — the address of the Param inside the
    coroutine frame.

  • Caller 保存着 Param* live — 也就是协程帧内的 Param 的地址。

Each side patches the other when it moves:

当任一方移动时,双方互相修补:

  • Param's copy/move constructor writes caller->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 writes live->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:

下面是一个可工作的示例:

https://godbolt.org/z/57orhTsxf

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章