Equivalent C++ to Python generator pattern

前端 未结 12 2350
Happy的楠姐
Happy的楠姐 2020-11-28 18:42

I\'ve got some example Python code that I need to mimic in C++. I do not require any specific solution (such as co-routine based yield solutions, although they would be acce

相关标签:
12条回答
  • 2020-11-28 18:55

    Something like this is very similar:

    struct pair_sequence
    {
        typedef pair<unsigned int, unsigned int> result_type;
        static const unsigned int limit = numeric_limits<unsigned int>::max()
    
        pair_sequence() : i(0), j(0) {}
    
        result_type operator()()
        {
            result_type r(i, j);
            if(j < limit) j++;
            else if(i < limit)
            {
              j = 0;
              i++;
            }
            else throw out_of_range("end of iteration");
        }
    
        private:
            unsigned int i;
            unsigned int j;
    }
    

    Using the operator() is only a question of what you want to do with this generator, you could also build it as a stream and make sure it adapts to an istream_iterator, for example.

    0 讨论(0)
  • 2020-11-28 18:58

    All answers that involve writing your own iterator are completely wrong. Such answers entirely miss the point of Python generators (one of the language's greatest and unique features). The most important thing about generators is that execution picks up where it left off. This does not happen to iterators. Instead, you must manually store state information such that when operator++ or operator* is called anew, the right information is in place at the very beginning of the next function call. This is why writing your own C++ iterator is a gigantic pain; whereas, generators are elegant, and easy to read+write.

    I don't think there is a good analog for Python generators in native C++, at least not yet (there is a rummor that yield will land in C++17). You can get something similarish by resorting to third-party (e.g. Yongwei's Boost suggestion), or rolling your own.

    I would say the closest thing in native C++ is threads. A thread can maintain a suspended set of local variables, and can continue execution where it left off, very much like generators, but you need to roll a little bit of additional infrastructure to support communication between the generator object and its caller. E.g.

    // Infrastructure
    
    template <typename Element>
    class Channel { ... };
    
    // Application
    
    using IntPair = std::pair<int, int>;
    
    void yield_pairs(int end_i, int end_j, Channel<IntPair>* out) {
      for (int i = 0; i < end_i; ++i) {
        for (int j = 0; j < end_j; ++j) {
          out->send(IntPair{i, j});  // "yield"
        }
      }
      out->close();
    }
    
    void MyApp() {
      Channel<IntPair> pairs;
      std::thread generator(yield_pairs, 32, 32, &pairs);
      for (IntPair pair : pairs) {
        UsePair(pair);
      }
      generator.join();
    }
    

    This solution has several downsides though:

    1. Threads are "expensive". Most people would consider this to be an "extravagant" use of threads, especially when your generator is so simple.
    2. There are a couple of clean up actions that you need to remember. These could be automated, but you'd need even more infrastructure, which again, is likely to be seen as "too extravagant". Anyway, the clean ups that you need are:
      1. out->close()
      2. generator.join()
    3. This does not allow you to stop generator. You could make some modifications to add that ability, but it adds clutter to the code. It would never be as clean as Python's yield statement.
    4. In addition to 2, there are other bits of boilerplate that are needed each time you want to "instantiate" a generator object:
      1. Channel* out parameter
      2. Additional variables in main: pairs, generator
    0 讨论(0)
  • 2020-11-28 18:59

    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 :)

    0 讨论(0)
  • 2020-11-28 19:01

    If you only need to do this for a relatively small number of specific generators, you can implement each as a class, where the member data is equivalent to the local variables of the Python generator function. Then you have a next function that returns the next thing the generator would yield, updating the internal state as it does so.

    This is basically similar to how Python generators are implemented, I believe. The major difference being they can remember an offset into the bytecode for the generator function as part of the "internal state", which means the generators can be written as loops containing yields. You would have to instead calculate the next value from the previous. In the case of your pair_sequence, that's pretty trivial. It may not be for complex generators.

    You also need some way of indicating termination. If what you're returning is "pointer-like", and NULL should not be a valid yieldable value you could use a NULL pointer as a termination indicator. Otherwise you need an out-of-band signal.

    0 讨论(0)
  • 2020-11-28 19:06

    Using range-v3:

    #include <iostream>
    #include <tuple>
    #include <range/v3/all.hpp>
    
    using namespace std;
    using namespace ranges;
    
    auto generator = [x = view::iota(0) | view::take(3)] {
        return view::cartesian_product(x, x);
    };
    
    int main () {
        for (auto x : generator()) {
            cout << get<0>(x) << ", " << get<1>(x) << endl;
        }
    
        return 0;
    }
    
    0 讨论(0)
  • 2020-11-28 19:10

    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.

    0 讨论(0)
提交回复
热议问题