There are two important concepts you'll need to comprehend before you can use coroutines effectively in C++. Number 1, the subroutines we're so used to programming (e.g. functions, procedures) lose their state when they return (You'll probably wonder that function objects also save state but there's a difference and that is: function objects stored their state in class member variables while coroutines store the state in the stack as automatic objects - these automatic objects are not destroyed btw which is COOL!). Number 2, coroutines preserved the point of execution and is able to resume execution from where it left off which is unlike function objects
Simple use of Coroutines
One example is the generator function (yep, python has it too) and what it is is really a function that returns a sequence of values instead of returning all of them; this programming concept is very useful in practice in the form of a dynamic prime number generator.
I love the function objects because it allows me to indulge in the world of functional programming to an extent though i know i'm still working in C++. You can pretty much recognize a function object in C++ by looking for the operator()() in the source code and possibly a couple of operators that are overloaded that suits the logic.
Simple example is the generator that generates all numbers (integers) between lower and upper inclusive. e.g. the lower bound is 1 and 4.
class generator {
public:
generator(int lower, int upper): lb(lower), ub(upper) {}
int operator()() { return lb++; }
operator bool() const { return lb < ub; }
private:
int lb, ub;
};
int main(int argc, char** argv) {
generator gen(1,4);
// invokes the operator bool()
while(gen)
// invokes the operator()() - function object!
std::cout << gen() << "\n";
}There's another way and that is using input iterators as generators but IMO, its not that straightforward and simple as opposed to using function objects which i do consider more elegance but then again, i'll iterate a philosophy of mine "whatever that works simply" when it comes to choosing paradigmsSo i know i can write generators using function objects and can i do that using coroutines? Sure i can and here's what i found out
// In a .h file, there's the contents #ifndef GEN_TEMPLATE_H #define GEN_TEMPLATE_H #includeNow this example is the input iterator fashion of creating a generator function and the creation of the generator is in the statement boost::bind(range_generator, _1, 100, 200) where the boost::coroutines::generatornamespace coro = boost::coroutines; typedef coro::generatorgen_type; /* Notice that range_generator body is entered for the first itme when the generator is constructed (from the main entry point) then at every iteration range_iterator is reentered from yield(). In this case range_iterator is reentered when generator::operator++ is invoked. */ int range_generator(gen_type::self& self, int min, int max) { while ( min < max - 1) self.yield(min++); self.exit(); // This is used instead } #endif ... ... // In another .cpp file #ifdef GEN_TEMPLATE_H gen_type generator(boost::bind(range_generator, _1, 100, 200) ); while( generator != gen_type() ) std::cout << *generator++ << "\n"; #endif
So i asked myself: "Great, it works on built-ins, how about user defined types?" Yep can be done too and here's what i did, its not great but it'll do for this example
// in some .h file
#ifndef GEN_TEMPLATE_H
#define GEN_TEMPLATE_H
class test {
public:
typedef int value_type;
test(int min) : m_current(min) {}
test() {}
test& operator++() {
m_current++;
if ( m_current > max ) m_current = -1;
return *this;
}
test& operator++(int) {
test t(*this);
++*this;
return t;
}
friend bool operator<(test& rhs, const test& lhs) {
if ( rhs.max != lhs.max ) rhs.max = lhs.max; // only invoked the first time
return rhs.m_current < lhs.m_current;
}
friend bool operator==(const test& rhs, const test& lhs) {
return rhs.m_current == lhs.m_current;
}
friend bool operator!=(const test& rhs, const test& lhs) {
return !(rhs == lhs);
}
operator int() { return m_current;}
int operator* () { return m_current; }
private:
int m_current;
int max;
};
#include
namespace coro = boost::coroutines;
typedef coro::generator gen_type;
/*
Notice that range_generator body is entered for the first itme when the generator is constructed
(from the main entry point) then at every iteration range_iterator is reentered from
yield(). In this case range_iterator is reentered when generator::operator++ is invoked.
*/
int range_generator(gen_type::self& self, test min, test max) {
while ( min < max)
self.yield(min++);
self.exit(); // This is used instead
}
#endif ...
...
// in some .c file
#ifdef GEN_TEMPLATE_H
gen_type generator(boost::bind(range_generator, _1, test(100), test(200)) );
while( generator != gen_type() )
std::cout << *generator++ << "\n";
#endif Pretty basic stuff there, so it can be done.Another concept: the producer/consumer pattern
I love this concept because its basically a messaging system and i have Nat Goodspeed & Brad Kittenbrink to thank ;) thanks guys!
Here's how it works (its not perfect code but it works to illustrate my example)
// In some .h file #ifndef MESSAGE_H #define MESSAGE_H #includeSo what i've basically done is to create a message queue of sorts and populate it with 10 simple messages and use the generator, in this case the consumer, to iterate through the messages.#include ... // and in some other .c file #ifdef MESSAGE_H consumer(gen(boost::bind(producer, _1, MessageQclass Message { public: Message(int msg) : m(msg) { std::cout << "Created:" << m << std::endl; } int operator *() { return m; } friend std::ostream& operator<<(std::ostream& os, const Message& m); private: int m; }; std::ostream& operator<<(std::ostream& os, const Message& m) { os << m.m << std::endl; return os; } template class MessageQ { public: MessageQ(int capacity = 10) : capacity(capacity) { for( int i = 0; i < capacity; ++i) q.push_back(Message(i)); } operator bool() { return q.size() > 0; } Message operator *() { Message lastEle = q.back(); q.pop_back(); return lastEle; } MessageQ& operator++() { if ( q.size() <= 0 ) return false; q.pop_back(); return *this; } private: std::vector q; int capacity; }; #endif #ifdef MESSAGE_H #include namespace coro = boost::coroutines; typedef coro::generator gen; const Message& producer(gen::self& self, MessageQ msgq) { while (msgq) { self.yield(*msgq); } self.exit(); } template void consumer(Producer prod) { do { std::cout << *prod << std::endl; } while( ++prod ); } #endif ... ()))); #endif
0 comments:
Post a Comment