非対称コルーチン
2つの非対称コルーチン型があります。coroutine<>::push_type
とcoroutine<>::pull_type
です。一方向のデータの転送を提供します。
注釈
asymmetric_coroutine<>
はcoroutine
のtypedefです。
coroutine<>::pull_type
coroutine<>::pull_type
は別の実行コンテキストからデータを転送します(引き出します)。テンプレート引数は転送されるパラメーターの型を定義します。coroutine<>::pull_type
のコンストラクターは、coroutine<>::push_type
への参照を引数にとる関数(コルーチン関数)を引数に取ります。coroutine<>::pull_type
をインスタンス化することで、実行の制御をコルーチン関数へと渡し、そして相補的なcoroutine<>::push_type
をライブラリが合成し、コルーチン関数へ参照として渡します。
この種のコルーチンはcoroutine<>::pull_type::operator()
を提供しています。このメソッドはコンテキストスイッチをするだけで、データを転送しません。
coroutine<>::pull_type
は、入力イテレーター(coroutine<>::pull_type::iterator
)とstd::begin()
/std::end()
のオーバーロードを提供します。インクリメント命令が、コンテキストの切り替え、並びにデータの転送を行います。
typedef boost::coroutines2::coroutine<int> coro_t;
coro_t::pull_type source(
[&](coro_t::push_type& sink){
int first=1,second=1;
sink(first);
sink(second);
for(int i=0;i<8;++i){
int third=first+second;
first=second;
second=third;
sink(third);
}
});
for(auto i:source)
std::cout << i << " ";
output:
1 1 2 3 5 8 13 21 34 55
この例では、coroutine<>::pull_type
が、main実行コンテキストにおいて、フィボナッチ数列を単純なforループで計算するラムダ関数(==コルーチン関数)をとることで作成されています。コルーチン関数は、coroutine<>::pull_type
のインスタンスにより管理される新しく作成された実行コンテキストで実行されます。coroutine<>::push_type
はライブラリにより自動的に生成され、ラムダ関数へ参照として渡されます。ラムダ関数の中でcoroutine<>::push_type::operator()
が別のフィボナッチ数とともに呼び出されるたびに、coroutine<>::push_type
がmain実行コンテキストへとそれを転送して戻ります。コルーチン関数のローカルな状態は保存され、次のフィボナッチ数を計算するべく元のコルーチン関数に実行制御を戻す際に復元されます。coroutine<>::pull_type
は入力イテレーターと std::begin()
/std::end()
のオーバーロードを提供しているので、範囲ベースforループを、生成されたフィボナッチ数列をイテレートするのに使うことが出来ます。
coroutine<>::push_type
coroutine<>::push_type
はデータを他の実行コンテキストへと転送します(押し入れます)。テンプレート引数は転送されるパラメーターの型を定義します。coroutine<>::push_type
のコンストラクターは、coroutine<>::pull_type
への参照を引数にとる関数(コルーチン関数)を引数に取ります。coroutine<>::pull_type
と異なり、coroutine<>::push_type
をインスタンス化しても、コルーチン関数へ実行の制御を渡しません。その代わり、coroutine<>::push_type::operator()
の最初の呼び出しが、相補的なcoroutine<>::pull_type
を合成し、コルーチン関数へ参照として渡します。
coroutine<>::push_type
のインターフェイスに、get()関数
は含まれません。つまり、この種のコルーチンでは、別の実行コンテキストから値を受け取ることは出来ません。
coroutine<>::push_type
は出力イテレーター(coroutine<>::push_type::iterator
)とstd::begin()
/std::end()
のオーバーロードを提供しています。インクリメント命令が、コンテキストの切り替え、並びにデータの転送を行います。
typedef boost::coroutines2::coroutine<std::string> coro_t;
struct FinalEOL{
~FinalEOL(){
std::cout << std::endl;
}
};
const int num=5, width=15;
coro_t::push_type writer(
[&](coro_t::pull_type& in){
// 何が起きたとしても、最終行を終了する
FinalEOL eol;
// 上流から値を引き出して、num個を一行に展開する
for (;;){
for(int i=0;i<num;++i){
// 入力を使い果たしたらやめる
if(!in) return;
std::cout << std::setw(width) << in.get();
// この要素をハンドルし終わったので、次へ進む
in();
}
// num個の要素の後、改行する
std::cout << std::endl;
}
});
std::vector<std::string> words{
"peas", "porridge", "hot", "peas",
"porridge", "cold", "peas", "porridge",
"in", "the", "pot", "nine",
"days", "old" };
std::copy(begin(words),end(words),begin(writer));
output:
peas porridge hot peas porridge
cold peas porridge in the
pot nine days old
この例では、coroutine<>::push_type
が、main実行コンテキストにおいて、文字列をリクエストして各行にnum個並べるラムダ関数(==コルーチン関数)をとることで作成されています。これは、コルーチンにより制御の逆転が可能になることを示しています。コルーチンなしでは、同じ仕事をするユーティリティー関数が新しい値をそれぞれ引数として受け取って、1つの値について処理した後に戻らなければなりません。その関数は静的な状態変数に依存するでしょう。しかしコルーチン関数では、関数を呼ぶかのように新しい値をリクエストすることができます。呼び出し元が関数を呼ぶかのように値を渡しているのにも関わらずです。コルーチン関数は、coroutine<>::push_type
のインスタンスにより管理される新しく作成された実行コンテキストで実行されます。coroutine<>::pull_type
がライブラリにより自動的に生成され、ラムダ関数へ参照として渡されます。コルーチン関数は、main実行コンテキストから渡された文字列へとcoroutine<>::pull_type::get()
を呼ぶことでアクセスして、パラメーターnumとwidthに従い、std::cout
で並べて出力します。コルーチン関数のローカルな状態は保存され、元のコルーチン関数に実行制御を戻す際に復元されます。coroutine<>::push_type
は入力イテレーターと std::begin()
/std::end()
のオーバーロードを提供しているので、std::copyアルゴリズムを、文字列をいれたvectorをイテレートしてひとつひとつコルーチンに渡すのに使うことが出来ます。
コルーチン関数
コルーチン関数はvoid
を返し、相対するコルーチンを引数として取ります。なのでコルーチン関数にとって、引数として渡されたコルーチンを使うことが、データを転送して実行制御を呼び出し元に戻す唯一の方法です。どちらのコルーチン型も、同じテンプレート引数を取ります。coroutine<>::pull_type
の場合、coroutine<>::pull_type
の作成時にコルーチン関数へと入ります。coroutine<>::push_type
の場合、coroutine<>::push_type
の作成時にコルーチン関数へと入りはしませんが、coroutine<>::push_type::operator()
の最初の発動時に入ります。実行制御がコルーチン関数から戻った後、コルーチンがまだ有効(コルーチン関数が終了していない)かどうかをcoroutine<>::pull_type::operator bool
がtrue
を返すかどうかで確かめることが出来ます。1番目のテンプレート引数がvoid
でない限り、true
はデータ値が利用可能であることも暗示します。
プル・コルーチンからmainコンテキストへのデータの転送
coroutine<>::pull_type
からmainコンテキストへデータを転送するために、フレームワークがmainコンテキストのcoroutine<>::pull_type
のインスタンスと関連付けられたcoroutine<>::push_type
を合成します。合成されたcoroutine<>::push_type
は、コルーチン関数へ引数として渡されます。コルーチン関数は、このcoroutine<>::push_type::operator()
を、各データをmainコンテキストへ戻すために呼び出さなければなりません。mainコンテキストでは、coroutine<>::pull_type::operator bool
により、コルーチンがまだ有効であるか否か、すなわち、データ値が利用可能であるかコルーチン関数が終了したかのどちらなのかを確かめます(coroutine<>::pull_type
が無効であるなら、データ値は利用可能ではありません)。転送されたデータ値へのアクセスは、coroutine<>::pull_type::get()
により行えます。
typedef boost::coroutines2::coroutine<int> coro_t;
coro_t::pull_type source( // コンストラクターによりコルーチン関数へ入る
[&](coro_t::push_type& sink){
sink(1); // {1} をmainコンテキストへ押し戻す
sink(1); // {1} をmainコンテキストへ押し戻す
sink(2); // {2} をmainコンテキストへ押し戻す
sink(3); // {3} をmainコンテキストへ押し戻す
sink(5); // {5} をmainコンテキストへ押し戻す
sink(8); // {8} をmainコンテキストへ押し戻す
});
while(source){ // プル・コルーチンが有効であるか確かめる
int ret=source.get(); // データ値へとアクセスする
source(); // コルーチン関数へとコンテキストスイッチする
}
mainコンテキストからプッシュ・コルーチンへのデータの転送
mainコンテキストからcoroutine<>::push_type
へデータを転送するために、フレームワークがmainコンテキストのcoroutine<>::push_type
のインスタンスと関連付けられたcoroutine<>::pull_type
を合成します。合成されたcoroutine<>::pull_type
はコルーチン関数へ引数として渡されます。mainコンテキストは、このcorotine<>::push_type::operator()
を、各データをコルーチン関数へ転送するために呼び出さなければなりません。転送されたデータ値へのアクセスは、coroutine<>::pull_type::get()
により行えます。
typedef boost::coroutines2::coroutine<int> coro_t;
coro_t::push_type sink( // コンストラクターはコルーチン関数に入らない
[&](coro_t::pull_type& source){
for (int i:source) {
std::cout << i << " ";
}
});
std::vector<int> v{1,1,2,3,5,8,13,21,34,55};
for( int i:v){
sink(i); // {i} をコルーチン関数へ押し入れる
}
パラメーターへのアクセス
コルーチン関数から返されたか、あるいはコルーチン関数へと転送されたデータへのアクセスは、coroutine<>::pull_type::get()
により行えます。
パラメーターへのアクセスをコンテキストスイッチ関数から分離したことで、coroutine<>::pull_type
が有効かの確認をcoroutine<>::pull_type::operator()
から戻ってきたあとで行えるようになりました。例えば、coroutine<>::pull_type
が値を持ち、かつコルーチン関数が終了していないことを確認できます。
typedef boost::coroutines2::coroutine<boost::tuple<int,int>> coro_t;
coro_t::push_type sink(
[&](coro_t::pull_type& source){
// タプル {7,11} へアクセス; x==7 y==1
int x,y;
boost::tie(x,y)=source.get();
});
sink(boost::make_tuple(7,11));
例外
coroutine<>::pull_type
のコルーチン関数内部で、coroutine<>::push_type::operator()
の最初の呼び出しの前に投げられた例外は、coroutine<>::pull_type
のコンストラクターにより再スローされます。coroutine<>::pull_type
のコルーチン関数の最初の呼び出しの後に、そのコルーチン関数内部で投げられる例外は全て、coroutine<>::pull_type::operator()
により再スローされます。coroutine<>::pull_type::get()
は例外を投げません。
coroutine<>::push_type
のコルーチン関数の内側で投げられた例外は、coroutine<>::push_type::operator()
により再スローされます。
重要
コルーチン関数により実行されるコードは、detail::forced_unwind
例外の伝播を妨げてはなりません。その例外を吸収してしまうと、スタックの巻き戻しの失敗を引き起こします。従って、すべての例外をキャッチするコードは全て、保留した全てのdetail::forced_unwind
例外を再スローしなくてはなりません。
try {
// スローするかも知れないコード
} catch(const boost::coroutines2::detail::forced_unwind&) {
throw;
} catch(...) {
// 保留した例外を再スローしなくとも良い
}
重要
catchブロック内から別の実行コンテキストへとjumpせずに、例外を再スローしてください。
スタックの巻き戻し
時には、ローカルスタック変数を破壊して割り当てられた資源を解放する(RAIIパターン)ために、未終了のコルーチンのスタックを巻き戻す必要があります。コルーチンのコンストラクターのattributes
引数は、デストラクターがスタックを巻き戻すべきかどうかを示しています(デフォルトではスタックは巻き戻されます)。
スタックの巻き戻しは以下を前提条件としています:
- コルーチンが「コルーチンでない」状態ではない
- コルーチンが完了していない
- コルーチンが実行中ではない
- コルーチンがスタックを持つ
巻き戻しの後、コルーチンは完了します。
struct X {
X(){
std::cout<<"X()"<<std::endl;
}
~X(){
std::cout<<"~X()"<<std::endl;
}
};
{
typedef boost::coroutines2::coroutine<void>::push_type coro_t;
coro_t::push_type sink(
[&](coro_t::pull_type& source){
X x;
for(int=0;;++i){
std::cout<<"fn(): "<<i<<std::endl;
// 実行制御をmain()へと戻します
source();
}
});
sink();
sink();
sink();
sink();
sink();
std::cout<<"sink is complete: "<<std::boolalpha<<!sink<<"\n";
}
output:
X()
fn(): 0
fn(): 1
fn(): 2
fn(): 3
fn(): 4
fn(): 5
sink is complete: false
~X()
範囲イテレーター
Boost.Coroutine2は、__boost_range__
を用いた出力イテレーターと入力イテレーターを提供します。coroutine<>::pull_type
は、入力イテレーターを介して、std::begin()
とstd::end()
を用いて使用可能です。
typedef boost::coroutines2::coroutine< int > coro_t;
int number=2,exponent=8;
coro_t::pull_type source(
[&](coro_t::push_type & sink){
int counter=0,result=1;
while(counter++<exponent){
result=result*number;
sink(result);
}
});
for (auto i:source)
std::cout << i << " ";
output:
2 4 8 16 32 64 128 256
coroutine<>::pull_type::iterator::operator++()
はcoroutine<>::pull_type::operator()
と対応していて、coroutine<>::pull_type::iterator::operator*()
はcoroutine<>::pull_type::get()
と概ね対応しています。coroutine<>::pull_type::operator bool
がfalse
を返すとき、元々はcoroutine<>::pull_type
のstd::begin()
から得たイテレーターと、同じcoroutine<>::pull_type
のインスタンスのstd::end()
で得たイテレーターは等しくなります。
注釈
もしT
がムーブ・オンリーな型であるなら、coroutine<T>::pull_type::iterator
の参照外しは、再びインクリメントされるまで1度だけしか出来ません。
出力イテレーターは、coroutine<>::push_type
により作成可能です。
typedef boost::coroutines2::coroutine<int> coro_t;
coro_t::push_type sink(
[&](coro_t::pull_type& source){
while(source){
std::cout << source.get() << " ";
source();
}
});
std::vector<int> v{1,1,2,3,5,8,13,21,34,55};
std::copy(begin(v),end(v),begin(sink));
coroutine<>::push_type::iterator::operator*()
は、coroutine<>::push_type::operator()
と概ね対応しています。coroutine<>::push_type::operator bool
がfalse
を返すとき、元々はcoroutine<>::push_type
のstd::begin()
から得たイテレーターと、同じcoroutine<>::push_type
のインスタンスのstd::end
で得たイテレーターは等しくなります。
コルーチン関数の終了
コルーチン関数を終了するには、単純なreturn文により呼び出しルーチンに戻ります。coroutine<>::pull_type
あるいはcoroutine<>::push_type
が完了したら、coroutine<>::pull_type::operator bool
あるいはcoroutine<>::push_type::operator bool
はfalse
を返します。
重要
コルーチン関数から戻った後は、コルーチンは完了しています(coroutine<>::push_type::operator()
やcoroutine<>::pull_type::operator()
で再開できません)。