如何簡單地用 Boost Coroutine 做出 Python yield
在 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)
的內容,而是:
- 呼叫的人 (callee) 先去做了
"do something"
, - 然後
next()
才會讓counter(15)
開始執行,輸出"print 0 in counter"
, - 該
next()
會回傳yield
的0*10
,所以會輸出0
, - 呼叫的人 (callee) 又去做其他事情,也就是
"do something else"
, - 該
next()
會回傳yield
的1*10
,所以會輸出1
, - 執行到
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}
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}
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
- 建立 coroutine 的瞬間,callee 就馬上呼叫了
counter(15)
開始執行,輸出"print 0 in counter"
, - Coroutine 在
yield
暫停了,呼叫的人 (callee) 開始做"do something"
, - Dereference 該 iterator 會回傳
yield
的0*10
,所以會輸出0
, - 以此類推。
順帶一題,上面這樣寫法有一個優點,就是可以完全保留函數原本的功能,不當 coroutine 使用。僅需要傳 nullptr
作為 push_type
就可以達成。
1counter(15); // yield default to nullptr