...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 - asymmetric_coroutine<>::push_type and asymmetric_coroutine<>::pull_type - provide a unidirectional transfer of data.
asymmetric_coroutine<>::pull_type transfers data from another execution context (== pulled-from). The template parameter defines the transferred parameter type. The constructor of asymmetric_coroutine<>::pull_type takes a function (coroutine-function) accepting a reference to an asymmetric_coroutine<>::push_type as argument. Instantiating an asymmetric_coroutine<>::pull_type passes the control of execution to coroutine-function and a complementary asymmetric_coroutine<>::push_type is synthesized by the library and passed as reference to coroutine-function.
This kind of coroutine provides asymmetric_coroutine<>::pull_type::operator(). This method only switches context; it transfers no data.
asymmetric_coroutine<>::pull_type provides input iterators (asymmetric_coroutine<>::pull_type::iterator) and std::begin()/std::end() are overloaded. The increment-operation switches the context and transfers data.
boost::coroutines::asymmetric_coroutine<int>::pull_type source( [&](boost::coroutines::asymmetric_coroutine<int>::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 asymmetric_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 asymmetric_coroutine<>::pull_type. An asymmetric_coroutine<>::push_type is automatically generated by the library and passed as reference to the lambda function. Each time the lambda function calls asymmetric_coroutine<>::push_type::operator() with another Fibonacci number, asymmetric_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 asymmetric_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.
asymmetric_coroutine<>::push_type transfers data to the other execution context (== pushed-to). The template parameter defines the transferred parameter type. The constructor of asymmetric_coroutine<>::push_type takes a function (coroutine-function) accepting a reference to an asymmetric_coroutine<>::pull_type as argument. In contrast to asymmetric_coroutine<>::pull_type, instantiating an asymmetric_coroutine<>::push_type does not pass the control of execution to coroutine-function - instead the first call of asymmetric_coroutine<>::push_type::operator() synthesizes a complementary asymmetric_coroutine<>::pull_type and passes it as reference to coroutine-function.
The asymmetric_coroutine<>::push_type interface does not contain a get()-function: you can not retrieve values from another execution context with this kind of coroutine.
asymmetric_coroutine<>::push_type provides output iterators (asymmetric_coroutine<>::push_type::iterator) and std::begin()/std::end() are overloaded. The increment-operation switches the context and transfers data.
struct FinalEOL{ ~FinalEOL(){ std::cout << std::endl; } }; const int num=5, width=15; boost::coroutines::asymmetric_coroutine<std::string>::push_type writer( [&](boost::coroutines::asymmetric_coroutine<std::string>::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(boost::begin(words),boost::end(words),boost::begin(writer)); output: peas porridge hot peas porridge cold peas porridge in the pot nine days old
In this example an asymmetric_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 asymmetric_coroutine<>::push_type. The main execution context passes the strings to the coroutine-function by calling asymmetric_coroutine<>::push_type::operator(). An asymmetric_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 asymmetric_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 asymmetric_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 asymmetric_coroutine<>::pull_type
the coroutine-function is entered at asymmetric_coroutine<>::pull_type
construction. For asymmetric_coroutine<>::push_type
the coroutine-function is not entered at asymmetric_coroutine<>::push_type
construction but entered by the first invocation of asymmetric_coroutine<>::push_type::operator().
After execution control is returned from coroutine-function
the state of the coroutine can be checked via asymmetric_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 asymmetric_coroutine<>::pull_type to the main-context the framework synthesizes an asymmetric_coroutine<>::push_type associated with the asymmetric_coroutine<>::pull_type instance in the main-context. The synthesized asymmetric_coroutine<>::push_type is passed as argument to coroutine-function. The coroutine-function must call this asymmetric_coroutine<>::push_type::operator() in order to transfer each data value back to the main-context. In the main-context, the asymmetric_coroutine<>::pull_type::operator bool determines whether the coroutine is still valid and a data value is available or coroutine-function has terminated (asymmetric_coroutine<>::pull_type is invalid; no data value available). Access to the transferred data value is given by asymmetric_coroutine<>::pull_type::get().
boost::coroutines::asymmetric_coroutine<int>::pull_type source( // constructor enters coroutine-function [&](boost::coroutines::asymmetric_coroutine<int>::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 asymmetric_coroutine<>::push_type from the main-context the framework synthesizes an asymmetric_coroutine<>::pull_type associated with the asymmetric_coroutine<>::push_type instance in the main-context. The synthesized asymmetric_coroutine<>::pull_type is passed as argument to coroutine-function. The main-context must call this asymmetric_coroutine<>::push_type::operator() in order to transfer each data value into the coroutine-function. Access to the transferred data value is given by asymmetric_coroutine<>::pull_type::get().
boost::coroutines::asymmetric_coroutine<int>::push_type sink( // constructor does NOT enter coroutine-function [&](boost::coroutines::asymmetric_coroutine<int>::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 asymmetric_coroutine<>::pull_type::get().
Splitting-up the access of parameters from context switch function enables to check if asymmetric_coroutine<>::pull_type is valid after return from asymmetric_coroutine<>::pull_type::operator(), e.g. asymmetric_coroutine<>::pull_type has values and coroutine-function has not terminated.
boost::coroutines::asymmetric_coroutine<boost::tuple<int,int>>::push_type sink( [&](boost::coroutines::asymmetric_coroutine<boost::tuple<int,int>>::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 asymmetric_coroutine<>::pull_type's coroutine-function before its first call to asymmetric_coroutine<>::push_type::operator() will be re-thrown by the asymmetric_coroutine<>::pull_type constructor. After an asymmetric_coroutine<>::pull_type's coroutine-function's first call to asymmetric_coroutine<>::push_type::operator(), any subsequent exception inside that coroutine-function will be re-thrown by asymmetric_coroutine<>::pull_type::operator(). asymmetric_coroutine<>::pull_type::get() does not throw.
An exception thrown inside an asymmetric_coroutine<>::push_type's coroutine-function will be re-thrown by asymmetric_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::coroutines::detail::forced_unwind&) { throw; } catch(...) { // possibly not re-throw pending exception }
Important | |
---|---|
Do not jump from inside a catch block and then 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; } }; { boost::coroutines::asymmetric_coroutine<void>::push_type sink( [&](boost::coroutines::asymmetric_coroutine<void>::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.Coroutine provides output- and input-iterators using Boost.Range. asymmetric_coroutine<>::pull_type can be used via input-iterators using std::begin() and std::end().
int number=2,exponent=8; boost::coroutines::asymmetric_coroutine< int >::pull_type source( [&]( boost::coroutines::asymmetric_coroutine< int >::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
asymmetric_coroutine<>::pull_type::iterator::operator++()
corresponds to asymmetric_coroutine<>::pull_type::operator();
asymmetric_coroutine<>::pull_type::iterator::operator*()
roughly corresponds to asymmetric_coroutine<>::pull_type::get().
An iterator originally obtained from std::begin() of
an asymmetric_coroutine<>::pull_type compares
equal to an iterator obtained from std::end() of that
same asymmetric_coroutine<>::pull_type instance
when its asymmetric_coroutine<>::pull_type::operator bool
would return false
].
Note | |
---|---|
If |
Output-iterators can be created from asymmetric_coroutine<>::push_type.
boost::coroutines::asymmetric_coroutine<int>::push_type sink( [&](boost::coroutines::asymmetric_coroutine<int>::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(boost::begin(v),boost::end(v),boost::begin(sink));
asymmetric_coroutine<>::push_type::iterator::operator*()
roughly corresponds to asymmetric_coroutine<>::push_type::operator().
An iterator originally obtained from std::begin() of
an asymmetric_coroutine<>::push_type compares
equal to an iterator obtained from std::end() of that
same asymmetric_coroutine<>::push_type instance
when its asymmetric_coroutine<>::push_type::operator bool
would return false
.
coroutine-function is exited with a simple return statement
jumping back to the calling routine. The asymmetric_coroutine<>::pull_type,
asymmetric_coroutine<>::push_type becomes complete,
e.g. asymmetric_coroutine<>::pull_type::operator bool,
asymmetric_coroutine<>::push_type::operator bool
will return false
.
Important | |
---|---|
After returning from coroutine-function the coroutine is complete (can not resumed with asymmetric_coroutine<>::push_type::operator(), asymmetric_coroutine<>::pull_type::operator()). |