如何簡單地用 Boost Coroutine 做出 Python yield

Share on:

本文內容採用創用 CC 姓名標示-非商業性-相同方式分享 3.0 台灣 授權條款授權.

在 Python 中,有 yield 這個關鍵字,讓我們把一個函數輕易的改寫成中間可以中斷執行的版本。在 C++ 中,C++20 以及 boost 也能透過 coroutine 來達成 yield 的效果。考量到 C++20 的支援可能尚未普及,我將會試著用 boost coroutine 來在盡量不改動原本 code 的情形下,模仿 Python 的 yield。

Python 的 yield

在 python 中,我們如果寫了這樣的函數:

1def counter(x: int):
2    for i in range(x):
3        print(f"print {i} in counter")
我們可以透過插入一個 yield 來讓這個函數變得可以在中途「中斷」:
 1def counter(x: int):
 2    for i in range(x):
 3        print(f"print {i} in counter")
 4        yield i*10
 5
 6f = counter(15)
 7print("do something")      # 1) doing something
 8v = next(f)                # 2) print 0 in counter
 9print(v)                   # 3) 0
10print("do something else") # 4) doing something else
11v = next(f)                # 5) print 1 in counter
12print(v)                   # 6) 10
例如說在這例子中 counter(15) 呼叫之後,並沒有開始真的執行 counter(15) 的內容,而是:

  1. 呼叫的人 (callee) 先去做了 "do something"
  2. 然後 next() 才會讓 counter(15) 開始執行,輸出 "print 0 in counter"
  3. next() 會回傳 yield0*10,所以會輸出 0
  4. 呼叫的人 (callee) 又去做其他事情,也就是 "do something else"
  5. next() 會回傳 yield1*10,所以會輸出 1
  6. 執行到 yield 之後又會中斷,並且把中斷的值回傳 0 給 callee,並輸出這個 0

C++ 的 yield

在 C++ 中,跟 Python 等效的程式碼如下:

1void counter(int x) {
2    for (int i = 0; i < x; ++i) {
3        cout << "print " << i << " in counter" << endl;
4    }
5}
下面的程式碼可以在不修改原本的程式碼(即,僅新增程式碼)的情形下,改裝成 coroutine 版本。由於 C++ 是強型別語言的關係,所以必須明確指定一種 yield 的型別,這邊我們設定為 int
 1#include <boost/coroutine/asymmetric_coroutine.hpp>
 2using boost::coroutines::coroutine;
 3
 4void counter(
 5    int x,
 6    coroutine<int>::push_type *yield = nullptr
 7) {
 8    for (int i = 0; i < x; ++i) {
 9        cout << "print " << i << " in counter" << endl;
10        if (yield) (*yield)(i*10);
11    }
12}
使用這個 coroutine 的方式如下,可以用 begin 這個函數把 counter 當作一個 iterator 來使用。
 1auto coro = coroutine<int>::pull_type(
 2    [](coroutine<int>::push_type &x) { counter(15, &x); }
 3);                                   // 1) print 0 in counter
 4int v; auto it = begin(coro);
 5cout << "do something" << endl;      // 2) do something
 6v = *it; ++it;                       // 3) print 1 in counter
 7cout << v << endl;                   // 4) 0
 8cout << "do something else" << endl; // 5) do something else
 9v = *it; ++it;                       // 6) print 2 in counter
10cout << v << endl;                   // 7) 10
注意到 C++ 版本中,前兩行的順序跟 Python 是相反的,原因是 boost 實做中,coroutine 建立的瞬間就會開始執行了:

  1. 建立 coroutine 的瞬間,callee 就馬上呼叫了 counter(15) 開始執行,輸出 "print 0 in counter"
  2. Coroutine 在 yield 暫停了,呼叫的人 (callee) 開始做 "do something"
  3. Dereference 該 iterator 會回傳 yield0*10,所以會輸出 0
  4. 以此類推。

順帶一題,上面這樣寫法有一個優點,就是可以完全保留函數原本的功能,不當 coroutine 使用。僅需要傳 nullptr 作為 push_type 就可以達成。

1counter(15); // yield default to nullptr