Equivalent C++ to Python generator pattern
Generators exist in C++, just under another name: Input Iterators. For example, reading from std::cin
is similar to having a generator of char
.
You simply need to understand what a generator does:
- there is a blob of data: the local variables define a state
- there is an init method
- there is a "next" method
- there is a way to signal termination
In your trivial example, it's easy enough. Conceptually:
struct State { unsigned i, j; };State make();void next(State&);bool isDone(State const&);
Of course, we wrap this as a proper class:
class PairSequence: // (implicit aliases) public std::iterator< std::input_iterator_tag, std::pair<unsigned, unsigned> >{ // C++03 typedef void (PairSequence::*BoolLike)(); void non_comparable();public: // C++11 (explicit aliases) using iterator_category = std::input_iterator_tag; using value_type = std::pair<unsigned, unsigned>; using reference = value_type const&; using pointer = value_type const*; using difference_type = ptrdiff_t; // C++03 (explicit aliases) typedef std::input_iterator_tag iterator_category; typedef std::pair<unsigned, unsigned> value_type; typedef value_type const& reference; typedef value_type const* pointer; typedef ptrdiff_t difference_type; PairSequence(): done(false) {} // C++11 explicit operator bool() const { return !done; } // C++03 // Safe Bool idiom operator BoolLike() const { return done ? 0 : &PairSequence::non_comparable; } reference operator*() const { return ij; } pointer operator->() const { return &ij; } PairSequence& operator++() { static unsigned const Max = std::numeric_limts<unsigned>::max(); assert(!done); if (ij.second != Max) { ++ij.second; return *this; } if (ij.first != Max) { ij.second = 0; ++ij.first; return *this; } done = true; return *this; } PairSequence operator++(int) { PairSequence const tmp(*this); ++*this; return tmp; }private: bool done; value_type ij;};
So hum yeah... might be that C++ is a tad more verbose :)
In C++ there are iterators, but implementing an iterator isn't straightforward: one has to consult the iterator concepts and carefully design the new iterator class to implement them. Thankfully, Boost has an iterator_facade template which should help implementing the iterators and iterator-compatible generators.
Sometimes a stackless coroutine can be used to implement an iterator.
P.S. See also this article which mentions both a switch
hack by Christopher M. Kohlhoff and Boost.Coroutine by Oliver Kowalke. Oliver Kowalke's work is a followup on Boost.Coroutine by Giovanni P. Deretta.
P.S. I think you can also write a kind of generator with lambdas:
std::function<int()> generator = []{ int i = 0; return [=]() mutable { return i < 10 ? i++ : -1; };}();int ret = 0; while ((ret = generator()) != -1) std::cout << "generator: " << ret << std::endl;
Or with a functor:
struct generator_t { int i = 0; int operator() () { return i < 10 ? i++ : -1; }} generator;int ret = 0; while ((ret = generator()) != -1) std::cout << "generator: " << ret << std::endl;
P.S. Here's a generator implemented with the Mordor coroutines:
#include <iostream>using std::cout; using std::endl;#include <mordor/coroutine.h>using Mordor::Coroutine; using Mordor::Fiber;void testMordor() { Coroutine<int> coro ([](Coroutine<int>& self) { int i = 0; while (i < 9) self.yield (i++); }); for (int i = coro.call(); coro.state() != Fiber::TERM; i = coro.call()) cout << i << endl;}
Since Boost.Coroutine2 now supports it very well (I found it because I wanted to solve exactly the same yield
problem), I am posting the C++ code that matches your original intention:
#include <stdint.h>#include <iostream>#include <memory>#include <boost/coroutine2/all.hpp>typedef boost::coroutines2::coroutine<std::pair<uint16_t, uint16_t>> coro_t;void pair_sequence(coro_t::push_type& yield){ uint16_t i = 0; uint16_t j = 0; for (;;) { for (;;) { yield(std::make_pair(i, j)); if (++j == 0) break; } if (++i == 0) break; }}int main(){ coro_t::pull_type seq(boost::coroutines2::fixedsize_stack(), pair_sequence); for (auto pair : seq) { print_pair(pair); } //while (seq) { // print_pair(seq.get()); // seq(); //}}
In this example, pair_sequence
does not take additional arguments. If it needs to, std::bind
or a lambda should be used to generate a function object that takes only one argument (of push_type
), when it is passed to the coro_t::pull_type
constructor.