协程中的默认引用参数和生命周期

Default reference parameters and lifetimes in coroutines

我对传递给 C++ 协程的参数的生命周期感到困惑。 回答a previous question,聪明人说

The lifetime of a parameter is [...] part of the caller's scope

现在,接下来,传递默认参数时会发生什么

generator my_coroutine(string&& s = string()) {...}

因此,如果 my_coroutine 是一个普通函数,那么 s 将在其整个范围内有效。但是,如果 my_coroutine 是协程,这似乎不再成立。

特别是以下协程测试的结果让我感到惊讶:

#include <iostream>
#include <coroutine>

struct Test {
    int i = 3;
    Test() { std::cout << "test constructed\n";}
    Test(const Test&) = delete;
    Test(Test&&) = delete;
    ~Test() { std::cout << "test destructed\n"; }
    friend std::ostream& operator<<(std::ostream& os, const Test& t) { return os << t.i; }
};

template<class T>
generator<int> coro_test(T&& t = T()) {
  int i = 0;
  while(i++ < 3) co_yield i;
  if(i == t.i) co_yield 100;
}

int main () {
  auto gen = coro_test<Test>();
  while(gen.is_valid()) {
    std::cout << *gen << "\n";
    ++gen;
  }
  return 0;
}

结果:

test constructed
test destructed
1
2
3

PS:为了完整起见,这是我的 generator:

template<class T>
struct generator {
  struct promise_type;
  using coro_handle = std::coroutine_handle<promise_type>;

  struct promise_type {
    T current_value;
    auto get_return_object() { return generator{coro_handle::from_promise(*this)}; }
    auto initial_suspend() const noexcept { return std::suspend_never{}; }
    auto final_suspend() const noexcept { return std::suspend_always{}; }
    void unhandled_exception() const { std::terminate(); }

    template<class Q>
    auto yield_value(Q&& value) {
      current_value = std::forward<Q>(value);
      return std::suspend_always{};
    }
  };
private:
  coro_handle coro;
  generator(coro_handle h): coro(h) {}
public:
  bool is_valid() const { return !coro.done(); }
  generator& operator++() { if(is_valid()) coro.resume(); return *this; }

  T& operator*() { return coro.promise().current_value; }
  const T& operator*() const { return coro.promise().current_value; }

  generator(const generator&) = delete;
  generator& operator=(const generator&) = delete;
  ~generator() { if(coro) coro.destroy(); }
};

正如“上一个问题”中指出的那样,协程中发生的第一件事是将参数“复制”到协程拥有的存储中。但是,“副本”最终是根据签名中声明的类型进行初始化的。也就是说,如果参数是引用,则该参数的“副本”也是引用。

因此,采用引用参数的协程函数与任何一种采用引用参数的异步函数非常相似:调用者必须确保引用对象始终存在该对象将被使用。初始化引用的默认参数是调用者无法控制其生命周期(除了提供显式参数)的情况。

您创建的 API 本身就已损坏。不要那样做。事实上,最好避免传递对任何类型的异步函数的引用,但如果这样做,则永远不要给它们默认参数。