最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++協(xié)程實(shí)現(xiàn)序列生成器的案例分享

 更新時間:2024年05月28日 09:27:32   作者:膚淺的羊  
序列生成器通常的實(shí)現(xiàn)是在一個協(xié)程內(nèi)部通過某種方式向外部傳一個值出去,并且將自己掛起,本文圍繞序列生成器這個經(jīng)典的協(xié)程案例介紹了協(xié)程的銷毀、co_await 運(yùn)算符、await_transform 以及 yield_value 的用法,需要的朋友可以參考下

目標(biāo)

序列生成器通常的實(shí)現(xiàn)是在一個協(xié)程內(nèi)部通過某種方式向外部傳一個值出去,并且將自己掛起,外部調(diào)用者則可以獲取到這個值,并且在后續(xù)繼續(xù)恢復(fù)執(zhí)行序列生成器來獲取下一個值。

掛起和向外部傳值的任務(wù)就需要通過co_await來完成,外部獲取值的任務(wù)就要通過協(xié)程的返回值來完成。

程序大概可以寫成下面這樣:

Generator sequence() {
  int i = 0;
  while (true) {
    co_await i++;
  }
}

int main() {
  auto generator = sequence();
  for (int i = 0; i < 10; ++i) {
    std::cout << generator.next() << std::endl;
  }
}

注意到 generator 有個 next 函數(shù),調(diào)用它時我們需要想辦法讓協(xié)程恢復(fù)執(zhí)行,并將下一個值傳出來。

好了,接下來我們就帶著這兩個問題去尋找解決辦法,順便把剩下的一點(diǎn)點(diǎn) C++ 協(xié)程的知識補(bǔ)齊。

調(diào)用者獲取協(xié)程產(chǎn)生的值

我們觀察main函數(shù)中的這段代碼:

int main() {
  auto generator = sequence();
  for (int i = 0; i < 10; ++i) {
    std::cout << generator.next() << std::endl;
  }
}

generator 的類型就是我們即將實(shí)現(xiàn)的序列生成器類型 Generator,結(jié)合上一篇文章當(dāng)中對于協(xié)程返回值類型的介紹,我們先大致給出它的定義:

struct Generator {
  struct promise_type {
    
    // 開始執(zhí)行時直接掛起等待外部調(diào)用 resume 獲取下一個值
    std::suspend_always initial_suspend() { return {}; };

    // 執(zhí)行結(jié)束后不需要掛起
    std::suspend_never final_suspend() noexcept { return {}; }

    // 為了簡單,我們認(rèn)為序列生成器當(dāng)中不會拋出異常,這里不做任何處理
    void unhandled_exception() { }

    // 構(gòu)造協(xié)程的返回值類型
    Generator get_return_object() {
      return Generator{};
    }

    // 沒有返回值
    void return_void() { }
  };

  int next() {
    ???.resume();
    return ???;
  }
};

代碼當(dāng)中有兩處我們標(biāo)注為 ???,表示暫時還不知道怎么處理。

如果我們想要在Generatorresume協(xié)程的話,需要拿到coroutine_handle,要如何做到這一點(diǎn)呢?

這個時候可以記住一點(diǎn),promise_type是鏈接協(xié)程內(nèi)外的橋梁,想要拿到什么,找promise_type要。標(biāo)準(zhǔn)庫提供了一個通過promise_type的對象的地址獲取coroutine_handle的函數(shù),它實(shí)際上是coroutine_handle的一個靜態(tài)函數(shù):

template <class _Promise>
struct coroutine_handle {
    static coroutine_handle from_promise(_Promise& _Prom) noexcept {
      ...
    }

    ...
}

這樣看來的話,我們只需要在get_return_object函數(shù)調(diào)用時,先獲取coroutine_handle,然后再傳給即將構(gòu)造出來的Generator就行,因此我們稍微修改一下前面的代碼:

struct Generator {
  struct promise_type {
    ...

    // 構(gòu)造協(xié)程的返回值類型
    Generator get_return_object() {
      return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) };
    }

    ...
  };

  std::coroutine_handle<promise_type> handle;

  int next() {
    handle.resume();
    return ???;
  }

};

接下來就是如何獲取協(xié)程內(nèi)部傳出來的值的問題了。同樣,本著有事找promise_type的原則,我們可以直接給它定義一個value成員:

struct Generator {
  struct promise_type {
    int value;

    ...
  };

  std::coroutine_handle<promise_type> handle;

  int next() {
    handle.resume();
    // 通過 handle 獲取 promise,然后再取到 value
    return handle.promise().value;
  }
};

協(xié)程內(nèi)部掛起并傳值

現(xiàn)在的問題就是如何從協(xié)程內(nèi)部傳值給promise_type了。

我們再觀察一下最終實(shí)現(xiàn)的效果:

Generator sequence() {
  int i = 0;
  while (true) {
    co_await i++;
  }
}

特別需要注意的是co_await i++;這一句,我們發(fā)現(xiàn)co_await后面的是一個整型值,而不是我們在前面文章中提到的滿足等待體(awaiter)條件的類型,這種情況下該怎么辦呢?

實(shí)際上,對于co_await <expr>表達(dá)式中expr的處理,C++有一套完善的流程:

  • 如果promise_type當(dāng)中定義了await_transform函數(shù),那么先通過promise.await_transform(expr)來對expr做一次轉(zhuǎn)換,得到的對象則為awaitable;否則awaitable就是expr本身。
  • 接下來使用awaitable對象獲取等待體(awaiter)。如果awaitable對象有operator co_await運(yùn)算符重載,那么等待體就是operator co_await(awaitable),否則等待體就是awaitable對象本身。

按照上面的說法,我們要么給promise_type實(shí)現(xiàn)一個await_transform(int)函數(shù),要么就為整型實(shí)現(xiàn)一個operator co_await的運(yùn)算符重載,二者選一個就可以了。

方案 1:實(shí)現(xiàn) operator co_await

這個方案就是給 int 定義 operator co_await 的重載:

auto operator co_await(int value) {
  struct IntAwaiter {
    int value;

    bool await_ready() const noexcept {
      return false;
    }
    void await_suspend(std::coroutine_handle<Generator::promise_type> handle) const {
      handle.promise().value = value;
    }
    void await_resume() {  }
  };
  return IntAwaiter{.value = value};
}

當(dāng)然,這個方案對于我們這個特定的場景下是行不通的,因?yàn)樵?C++ 當(dāng)中我們是無法給基本類型定義運(yùn)算符重載的。

不過,如果我們遇到的情況不是基本類型,那么運(yùn)算符重載的思路就可以行得通。operator co_await 的重載我們將會在后面給出例子。

方案 2:await_transform

運(yùn)算符重載行不通,那就只能通過 await_tranform 來做轉(zhuǎn)換了。

代碼比較簡單:

struct Generator {
  struct promise_type {
    int value;

    // 傳值的同時要掛起,值存入 value 當(dāng)中
    std::suspend_always await_transform(int value) {
      this->value = value;
      return {};
    }

    ...
  };

  std::coroutine_handle<promise_type> handle;

  int next() {
    handle.resume();

  // 外部調(diào)用者或者恢復(fù)者可以通過讀取 value
    return handle.promise().value;
  }
};

定義了 await_transform 函數(shù)之后,co_await expr 就相當(dāng)于 co_await promise.await_transform(expr) 了。

至此,我們的例子就可以運(yùn)行了:

Generator sequence() {
  int i = 0;
  while (true) {
    co_await i++;
  }
}

int main() {
  auto gen = sequence();
  for (int i = 0; i < 5; ++i) {
    std::cout << gen.next() << std::endl;
  }
}

運(yùn)行結(jié)果如下:

0
1
2
3
4

協(xié)程的銷毀

雖然我們的協(xié)程已經(jīng)能夠正常工作,但它仍然存在一些問題。

問題1: 無法確定是否存在下一個元素

當(dāng)外部調(diào)用者或者恢復(fù)者試圖調(diào)用 next 來獲取下一個元素的時候,它其實(shí)并不知道能不能真的得到一個結(jié)果。程序也可能拋出異常:

如下例:

Generator sequence() {
  int i = 0;
  // 只傳出 5 個值
  while (i < 5) {
    co_await i++;
  }
}

int main() {
  auto gen = sequence();
  for (int i = 0; i < 15; ++i) {
    // 試圖讀取 15 個值
    std::cout << gen.next() << std::endl;
  }
  return 0;
}

程序的結(jié)果是什么呢?

0
1
2
3
4
4

Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

最后一個輸出的 4 實(shí)際上是恰好遇到協(xié)程銷毀之前的狀態(tài),此時 promise 當(dāng)中的 value 值還是之前的 4。而當(dāng)我們試圖不斷的去讀取協(xié)程的值,程序就拋出 SIGSEGV 的錯誤。錯誤的原因你可能已經(jīng)想到了,當(dāng)協(xié)程體執(zhí)行完之后,協(xié)程的狀態(tài)就會被銷毀,如果我們再訪問協(xié)程的話,就相當(dāng)于訪問了一個野指針。

為了解決這個問題,我們需要增加一個 has_next 函數(shù),用來判斷是否還有新的值傳出來,has_next 函數(shù)調(diào)用的時候有兩種情況:

  • 已經(jīng)有一個值傳出來了,還沒有被外部消費(fèi)
  • 還沒有現(xiàn)成的值可以用,需要嘗試恢復(fù)執(zhí)行協(xié)程來看看還有沒有下一個值傳出來

這里我們需要有一種有效的辦法來判斷 value 是不是有效的,單憑 value 本身我們其實(shí)是無法確定它的值是不是被消費(fèi)了,因此我們需要加一個值來存儲這個狀態(tài):

struct Generator {

  // 協(xié)程執(zhí)行完成之后,外部讀取值時拋出的異常
  class ExhaustedException: std::exception { };

  struct promise_type {
    int value;
    bool is_ready = false;
    ...
  }
  ...
}

我們定義一個成員 state 來記錄協(xié)程執(zhí)行的狀態(tài),狀態(tài)的類型一共三種,只有 READY 的時候我們才能拿到值。

接下來改造 next 函數(shù),同時增加 has_next 函數(shù)來描述協(xié)程是否仍然可以有值傳出:

struct Generator {
  ...

  bool has_next() {
    // 協(xié)程已經(jīng)執(zhí)行完成
    if (handle.done()) {
      return false;
    }

    // 協(xié)程還沒有執(zhí)行完成,并且下一個值還沒有準(zhǔn)備好
    if (!handle.promise().is_ready) {
      handle.resume();
    }

    if (handle.done()) {
      // 恢復(fù)執(zhí)行之后協(xié)程執(zhí)行完,這時候必然沒有通過 co_await 傳出值來
      return false;
    } else {
      return true;
    }
  }

  int next() {
    if (has_next()) {
      // 此時一定有值,is_ready 為 true 
      // 消費(fèi)當(dāng)前的值,重置 is_ready 為 false
      handle.promise().is_ready = false;
      return handle.promise().value;
    }
    throw ExhaustedException();
  }
};

注意,我們在promise_type中產(chǎn)生新值后需要將is_ready置為true如下:

struct Generator {
  ...

  struct promise_type {
    ...

    std::suspend_always await_transform(T value) {
      this->value = value;
      is_ready = true;
      return {};
    }
    
  };
};

這樣外部使用時就需要先通過 has_next 來判斷是否有下一個值,然后再去讀取了:

...

int main() {
  auto generator = sequence();
  for (int i = 0; i < 15; ++i) {
    if (generator.has_next()) {
      std::cout << generator.next() << std::endl;
    } else {
      break;
    }
  }
  return 0;
}

問題2:協(xié)程狀態(tài)的銷毀比Generator對象的銷毀更早

我們前面提到過,協(xié)程的狀態(tài)在協(xié)程體執(zhí)行完之后就會銷毀,除非協(xié)程掛起在 final_suspend 調(diào)用時。

我們的例子當(dāng)中 final_suspend 返回了 std::suspend_never,因此協(xié)程的銷毀時機(jī)其實(shí)比 Generator 更早:

auto generator = sequence();
for (int i = 0; i < 15; ++i) {
  if (generator.has_next()) {
    std::cout << generator.next() << std::endl;
  } else {
    // 協(xié)程已經(jīng)執(zhí)行完,協(xié)程的狀態(tài)已經(jīng)銷毀
    break;
  }
}

// generator 對象在此仍然有效

這看上去似乎問題不大,因?yàn)槲覀冊谇懊嫱ㄟ^ has_next 的判斷保證了讀取值的安全性。

但實(shí)際上情況并非如此。我們在 has_next 當(dāng)中調(diào)用了 coroutine_handle::done 來判斷協(xié)程體是否執(zhí)行完成,判斷之前很可能協(xié)程已經(jīng)銷毀,coroutine_handle 這時候都已經(jīng)是無效的了:

bool has_next() {
  // 如果協(xié)程已經(jīng)執(zhí)行完成,理論上協(xié)程的狀態(tài)已經(jīng)銷毀,handle 指向的是一個無效的協(xié)程
  // 如果 handle 本身已經(jīng)無效,因此 done 函數(shù)的調(diào)用此時也是無效的
  if (handle.done()) {
    return false;
  }
  ...
}

因此為了讓協(xié)程的狀態(tài)的生成周期與 Generator 一致,我們必須將協(xié)程的銷毀交給 Generator 來處理:

struct Generator {

  class ExhaustedException: std::exception { };

  struct promise_type {
    ...

    // 總是掛起,讓 Generator 來銷毀
    std::suspend_always final_suspend() noexcept { return {}; }

    ...
  };

  ...

  ~Generator() {
    // 銷毀協(xié)程
    handle.destroy();
  }
};

問題3:復(fù)制對象導(dǎo)致協(xié)程被銷毀

這個問題確切地說是問題 2的解決方案不完善引起的。

我們在 Generator 的析構(gòu)函數(shù)當(dāng)中銷毀協(xié)程,這本身沒有什么問題,但如果我們把 Generator 對象做一下復(fù)制,例如從一個函數(shù)當(dāng)中返回,情況可能就會變得復(fù)雜。例如:

Generator returns_generator() {
  auto g = sequence();
  if (g.has_next()) {
    std::cout << g.next() << std::endl;
  }
  return g;
}

int main() {
  auto generator = returns_generator();
  for (int i = 0; i < 15; ++i) {
    if (generator.has_next()) {
      std::cout << generator.next() << std::endl;
    } else {
      break;
    }
  }
  return 0;
}

這段代碼乍一看似乎沒什么問題,但由于我們把 g 當(dāng)做返回值返回了,這時候 g 這個對象就發(fā)生了一次復(fù)制,然后臨時對象被銷毀。接下來的事兒大家就很容易想到了,運(yùn)行結(jié)果如下:

0
-572662307

Process finished with exit code -1073741819 (0xC0000005)

為了解決這個問題,我們需要妥善地處理 Generator 的復(fù)制構(gòu)造器:

struct Generator {
  ...

  explicit Generator(std::coroutine_handle<promise_type> handle) noexcept
      : handle(handle) {}

  Generator(Generator &&generator) noexcept
      : handle(std::exchange(generator.handle, {})) {}

  Generator(Generator &) = delete;
  Generator &operator=(Generator &) = delete;

  ~Generator() {
    if (handle) handle.destroy();
  }
}

我們只提供了右值復(fù)制構(gòu)造器,對于左值復(fù)制構(gòu)造器,我們直接刪除掉以禁止使用。原因也很簡單,對于每一個協(xié)程實(shí)例,都有且僅能有一個 Generator 實(shí)例與之對應(yīng),因此我們只支持移動對象,而不支持復(fù)制對象。

使用co_yield

序列生成器這個需求的實(shí)現(xiàn)其實(shí)有個更好的選擇,那就是使用 co_yieldco_yield 就是專門為向外傳值來設(shè)計的,如果大家對其他語言的協(xié)程有了解,也一定見到過各種 yield 的實(shí)現(xiàn)。

C++ 當(dāng)中的 co_yield expr 等價于 co_await promise.yield_value(expr),我們只需要將前面例子當(dāng)中的 await_transform 函數(shù)替換成 yield_value 就可以使用 co_yield 來傳值了:

struct Generator {

  class ExhaustedException: std::exception { };

  struct promise_type {
    ...

    // 將 await_transform 替換為 yield_value
    std::suspend_always yield_value(int value) {
      this->value = value;
      is_ready = true;
      return {};
    }
    ...
  };
  ...
};

Generator sequence() {
  int i = 0;
  while (i < 5) {
    // 使用 co_yield 來替換 co_await
    co_yield i++;
  }
}

可以看到改動點(diǎn)非常少,運(yùn)行效果與前面的例子一致。

盡管可以實(shí)現(xiàn)相同的效果,但通常情況下我們使用 co_await 更多的關(guān)注點(diǎn)在掛起自己,等待別人上,而使用 co_yield 則是掛起自己傳值出去。因此我們應(yīng)該針對合適的場景做出合適的選擇。

使用序列生成器生成fibonacci數(shù)列

接下來我們要使用序列生成器來實(shí)現(xiàn)一個更有意義的例子,即斐波那契數(shù)列。

Generator fibonacci() {
  co_yield 0; // fib(0)
  co_yield 1; // fib(1)

  int a = 0;
  int b = 1;
  while(true) {
    co_yield a + b; // fib(N), N > 1
    b = a + b;
    a = b - a;
  }
}

我們看到這個實(shí)現(xiàn)非常的直接,完全不需要考慮 fib(N - 1) 和 fib(N - 2) 的存儲問題。

如果沒有協(xié)程,我們的實(shí)現(xiàn)可能是這樣的:

class Fibonacci {
 public:
  int next() {
    // 初值不符合整體的規(guī)律,需要單獨(dú)處理
    if (a == -1){
      a = 0;
      b = 1;
      return 0;
    }

    int next = b;
    b = a + b;
    a = b - a;
    return next;
  }

 private:
  int a = -1;
  int b = 0;
};

使用時先構(gòu)造一個 Fibonacci 對象,然后調(diào)用 next 函數(shù)來獲取下一個值。對比之下,協(xié)程的實(shí)現(xiàn)帶來的好處是顯而易見的。協(xié)程生成的這個序列是個懶序列(Lazy Sequence),沒用到的項就不會被生成。

總結(jié)

本文圍繞序列生成器這個經(jīng)典的協(xié)程案例介紹了協(xié)程的銷毀、co_await 運(yùn)算符、await_transform 以及 yield_value 的用法。

以上就是C++協(xié)程實(shí)現(xiàn)序列生成器的案例分享的詳細(xì)內(nèi)容,更多關(guān)于C++序列生成器的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • C語言利用棧實(shí)現(xiàn)對后綴表達(dá)式的求解

    C語言利用棧實(shí)現(xiàn)對后綴表達(dá)式的求解

    這篇文章主要為大家詳細(xì)介紹了C語言利用棧實(shí)現(xiàn)對后綴表達(dá)式的求解,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-04-04
  • 基于C語言實(shí)現(xiàn)點(diǎn)餐系統(tǒng)

    基于C語言實(shí)現(xiàn)點(diǎn)餐系統(tǒng)

    這篇文章主要為大家詳細(xì)介紹了基于C語言實(shí)現(xiàn)點(diǎn)餐系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-11-11
  • C++動態(tài)內(nèi)存管理詳解

    C++動態(tài)內(nèi)存管理詳解

    今天小編就為大家分享一篇關(guān)于關(guān)于C++動態(tài)分配內(nèi)存的介紹,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2021-08-08
  • C++類繼承時的構(gòu)造函數(shù)

    C++類繼承時的構(gòu)造函數(shù)

    這篇文章主要介紹了C++類繼承時的構(gòu)造函數(shù),C++中,子類繼承父類除去構(gòu)造函數(shù)和析構(gòu)函數(shù)以外的所有成員。因此,子類需要編寫自己的構(gòu)造函數(shù)和析構(gòu)函數(shù)。更多相關(guān)詳情需要的小伙伴可以參考下面文章介紹
    2022-03-03
  • 深度解析三個常見的C語言內(nèi)存函數(shù)

    深度解析三個常見的C語言內(nèi)存函數(shù)

    這篇文章主要深度解析了三個常見的C語言內(nèi)存函數(shù)memcpy,memmove,memcmp,所以本文將對memcpy,memmove,memcmp 三個函數(shù)進(jìn)行詳解和模擬實(shí)現(xiàn),需要的朋友可以參考下
    2023-07-07
  • C++小知識:大于0并不意味著等于1

    C++小知識:大于0并不意味著等于1

    今天小編就為大家分享一篇關(guān)于C++小知識:大于0并不意味著等于1,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-01-01
  • C 語言中enum枚舉的定義和使用小結(jié)

    C 語言中enum枚舉的定義和使用小結(jié)

    在 C 語言里,enum(枚舉)是一種用戶自定義的數(shù)據(jù)類型,它能夠讓你創(chuàng)建一組具名的整數(shù)常量,下面我會從定義、使用、特性等方面詳細(xì)介紹enum,感興趣的朋友一起看看吧
    2025-04-04
  • 使用Visual Studio進(jìn)行動態(tài)鏈接庫開發(fā)流程

    使用Visual Studio進(jìn)行動態(tài)鏈接庫開發(fā)流程

    這篇文章主要介紹了使用Visual Studio進(jìn)行動態(tài)鏈接庫開發(fā)流程,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-05-05
  • C語言實(shí)現(xiàn)從文件讀入一個3*3數(shù)組,并計算每行的平均值

    C語言實(shí)現(xiàn)從文件讀入一個3*3數(shù)組,并計算每行的平均值

    今天小編就為大家分享一篇C語言實(shí)現(xiàn)從文件讀入一個3*3數(shù)組,并計算每行的平均值,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-12-12
  • C語言中switch語句基本用法實(shí)例

    C語言中switch語句基本用法實(shí)例

    switch的中文翻譯是開關(guān),顧名思義,開關(guān)的作用就是控制連通或者中斷,在C語言中switch語句的作用也是大同小異,下面這篇文章主要給大家介紹了關(guān)于C語言中switch語句基本用法的相關(guān)資料,需要的朋友可以參考下
    2022-07-07

最新評論

什邡市| 高清| 日土县| 远安县| 理塘县| 洛浦县| 栾城县| 平湖市| 莱州市| 邳州市| 平陆县| 治多县| 中山市| 宣城市| 呼和浩特市| 南乐县| 东乌珠穆沁旗| 当涂县| 西盟| 当阳市| 柏乡县| 从江县| 奉新县| 乌拉特前旗| 迭部县| 巴彦县| 通城县| 五家渠市| 黑水县| 星座| 安陆市| 新竹市| 建湖县| 岳阳县| 勐海县| 格尔木市| 敖汉旗| 兴城市| 平江县| 曲松县| 华池县|