...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Two asymmetric coroutine types - coroutine<>::push_type and coroutine<>::pull_type - provide a unidirectional transfer of data.
Note | |
---|---|
asymmetric_coroutine<> is a typedef of coroutine. |
coroutine<>::pull_type transfers data from another execution context (== pulled-from). The template parameter defines the transferred parameter type. The constructor of coroutine<>::pull_type takes a function (coroutine-function) accepting a reference to an coroutine<>::push_type as argument. Instantiating an coroutine<>::pull_type passes the control of execution to coroutine-function and a complementary coroutine<>::push_type is synthesized by the library and passed as reference to coroutine-function.
This kind of coroutine provides coroutine<>::pull_type::operator(). This method only switches context; it transfers no data.
coroutine<>::pull_type provides input iterators (coroutine<>::pull_type::iterator) and std::begin()/std::end() are overloaded. The increment-operation switches the context and transfers data.
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
In this example an coroutine<>::pull_type is created in the main execution context taking a lambda function (== coroutine-function) which calculates Fibonacci numbers in a simple for-loop. The coroutine-function is executed in a newly created execution context which is managed by the instance of coroutine<>::pull_type. An coroutine<>::push_type is automatically generated by the library and passed as reference to the lambda function. Each time the lambda function calls coroutine<>::push_type::operator() with another Fibonacci number, coroutine<>::push_type transfers it back to the main execution context. The local state of coroutine-function is preserved and will be restored upon transferring execution control back to coroutine-function to calculate the next Fibonacci number. Because coroutine<>::pull_type provides input iterators and std::begin()/std::end() are overloaded, a range-based for-loop can be used to iterate over the generated Fibonacci numbers.
coroutine<>::push_type transfers data to the other execution context (== pushed-to). The template parameter defines the transferred parameter type. The constructor of coroutine<>::push_type takes a function (coroutine-function) accepting a reference to an coroutine<>::pull_type as argument. In contrast to coroutine<>::pull_type, instantiating an coroutine<>::push_type does not pass the control of execution to coroutine-function - instead the first call of coroutine<>::push_type::operator() synthesizes a complementary coroutine<>::pull_type and passes it as reference to coroutine-function.
The coroutine<>::push_type interface does not contain a get()-function: you can not retrieve values from another execution context with this kind of coroutine.
coroutine<>::push_type provides output iterators (coroutine<>::push_type::iterator) and std::begin()/std::end() are overloaded. The increment-operation switches the context and transfers data.
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){ // finish the last line when we leave by whatever means FinalEOL eol; // pull values from upstream, lay them out 'num' to a line for (;;){ for(int i=0;i<num;++i){ // when we exhaust the input, stop if(!in) return; std::cout << std::setw(width) << in.get(); // now that we've handled this item, advance to next in(); } // after 'num' items, line break 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
In this example an coroutine<>::push_type is created in the main execution context accepting a lambda function (== coroutine-function) which requests strings and lays out 'num' of them on each line. This demonstrates the inversion of control permitted by coroutines. Without coroutines, a utility function to perform the same job would necessarily accept each new value as a function parameter, returning after processing that single value. That function would depend on a static state variable. A coroutine-function, however, can request each new value as if by calling a function -- even though its caller also passes values as if by calling a function. The coroutine-function is executed in a newly created execution context which is managed by the instance of coroutine<>::push_type. The main execution context passes the strings to the coroutine-function by calling coroutine<>::push_type::operator(). An coroutine<>::pull_type instance is automatically generated by the library and passed as reference to the lambda function. The coroutine-function accesses the strings passed from the main execution context by calling coroutine<>::pull_type::get() and lays those strings out on std::cout according the parameters 'num' and 'width'. The local state of coroutine-function is preserved and will be restored after transferring execution control back to coroutine-function. Because coroutine<>::push_type provides output iterators and std::begin()/std::end() are overloaded, the std::copy algorithm can be used to iterate over the vector containing the strings and pass them one by one to the coroutine.
The coroutine-function returns void
and takes its counterpart-coroutine as argument, so that using the coroutine
passed as argument to coroutine-function is the only
way to transfer data and execution control back to the caller. Both coroutine
types take the same template argument. For coroutine<>::pull_type
the coroutine-function is entered at coroutine<>::pull_type
construction. For coroutine<>::push_type the
coroutine-function is not entered at coroutine<>::push_type
construction but entered by the first invocation of coroutine<>::push_type::operator().
After execution control is returned from coroutine-function
the state of the coroutine can be checked via coroutine<>::pull_type::operator
bool returning true
if the coroutine is still valid (coroutine-function
has not terminated). Unless the first template parameter is void
, true
also implies that a data value is available.
In order to transfer data from an coroutine<>::pull_type to the main-context the framework synthesizes an coroutine<>::push_type associated with the coroutine<>::pull_type instance in the main-context. The synthesized coroutine<>::push_type is passed as argument to coroutine-function. The coroutine-function must call this coroutine<>::push_type::operator() in order to transfer each data value back to the main-context. In the main-context, the coroutine<>::pull_type::operator bool determines whether the coroutine is still valid and a data value is available or coroutine-function has terminated (coroutine<>::pull_type is invalid; no data value available). Access to the transferred data value is given by coroutine<>::pull_type::get().
typedef boost::coroutines2::coroutine<int> coro_t; coro_t::pull_type source( // constructor enters coroutine-function [&](coro_t::push_type& sink){ sink(1); // push {1} back to main-context sink(1); // push {1} back to main-context sink(2); // push {2} back to main-context sink(3); // push {3} back to main-context sink(5); // push {5} back to main-context sink(8); // push {8} back to main-context }); while(source){ // test if pull-coroutine is valid int ret=source.get(); // access data value source(); // context-switch to coroutine-function }
In order to transfer data to an coroutine<>::push_type from the main-context the framework synthesizes an coroutine<>::pull_type associated with the coroutine<>::push_type instance in the main-context. The synthesized coroutine<>::pull_type is passed as argument to coroutine-function. The main-context must call this coroutine<>::push_type::operator() in order to transfer each data value into the coroutine-function. Access to the transferred data value is given by coroutine<>::pull_type::get().
typedef boost::coroutines2::coroutine<int> coro_t; coro_t::push_type sink( // constructor does NOT enter coroutine-function [&](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); // push {i} to coroutine-function }
Parameters returned from or transferred to the coroutine-function can be accessed with coroutine<>::pull_type::get().
Splitting-up the access of parameters from context switch function enables to check if coroutine<>::pull_type is valid after return from coroutine<>::pull_type::operator(), e.g. coroutine<>::pull_type has values and coroutine-function has not terminated.
typedef boost::coroutines2::coroutine<boost::tuple<int,int>> coro_t; coro_t::push_type sink( [&](coro_t::pull_type& source){ // access tuple {7,11}; x==7 y==1 int x,y; boost::tie(x,y)=source.get(); }); sink(boost::make_tuple(7,11));
An exception thrown inside an coroutine<>::pull_type's coroutine-function before its first call to coroutine<>::push_type::operator() will be re-thrown by the coroutine<>::pull_type constructor. After an coroutine<>::pull_type's coroutine-function's first call to coroutine<>::push_type::operator(), any subsequent exception inside that coroutine-function will be re-thrown by coroutine<>::pull_type::operator(). coroutine<>::pull_type::get() does not throw.
An exception thrown inside an coroutine<>::push_type's coroutine-function will be re-thrown by coroutine<>::push_type::operator().
Important | |
---|---|
Code executed by coroutine-function must not prevent the propagation of the detail::forced_unwind exception. Absorbing that exception will cause stack unwinding to fail. Thus, any code that catches all exceptions must re-throw any pending detail::forced_unwind exception. |
try { // code that might throw } catch(const boost::coroutines2::detail::forced_unwind&) { throw; } catch(...) { // possibly not re-throw pending exception }
Important | |
---|---|
Do not jump from inside a catch block and than re-throw the exception in another execution context. |
Sometimes it is necessary to unwind the stack of an unfinished coroutine
to destroy local stack variables so they can release allocated resources
(RAII pattern). The attributes
argument of the coroutine constructor indicates whether the destructor should
unwind the stack (stack is unwound by default).
Stack unwinding assumes the following preconditions:
After unwinding, a coroutine is complete.
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; // transfer execution control back to 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 provides output- and input-iterators using __boost_range__. coroutine<>::pull_type can be used via input-iterators using std::begin() and 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++()
corresponds to coroutine<>::pull_type::operator();
coroutine<>::pull_type::iterator::operator*()
roughly corresponds to coroutine<>::pull_type::get().
An iterator originally obtained from std::begin() of
an coroutine<>::pull_type compares equal to an
iterator obtained from std::end() of that same coroutine<>::pull_type
instance when its coroutine<>::pull_type::operator bool
would return false
].
Note | |
---|---|
If |
Output-iterators can be created from 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*()
roughly corresponds to coroutine<>::push_type::operator().
An iterator originally obtained from std::begin() of
an coroutine<>::push_type compares equal to an
iterator obtained from std::end() of that same coroutine<>::push_type
instance when its coroutine<>::push_type::operator bool
would return false
.
coroutine-function is exited with a simple return statement
jumping back to the calling routine. The coroutine<>::pull_type,
coroutine<>::push_type becomes complete, e.g.
coroutine<>::pull_type::operator bool, coroutine<>::push_type::operator
bool will return false
.
Important | |
---|---|
After returning from coroutine-function the coroutine is complete (can not resumed with coroutine<>::push_type::operator(), coroutine<>::pull_type::operator()). |