【问题标题】:Equivalent C++ to Python generator pattern等效于 C++ 到 Python 生成器模式
【发布时间】:2019-05-03 16:07:11
【问题描述】:

我有一些需要在 C++ 中模仿的示例 Python 代码。我不需要任何特定的解决方案(例如基于协程的产量解决方案,尽管它们也是可以接受的答案),我只需要以某种方式重现语义。

Python

这是一个基本的序列生成器,显然太大而无法存储具体化版本。

def pair_sequence():
    for i in range(2**32):
        for j in range(2**32):
            yield (i, j)

目标是维护上述序列的两个实例,并以半同步的方式迭代它们,但以块的形式进行。在下面的示例中,first_pass 使用对序列来初始化缓冲区,second_pass 重新生成完全相同的序列并再次处理缓冲区。

def run():
    seq1 = pair_sequence()
    seq2 = pair_sequence()

    buffer = [0] * 1000
    first_pass(seq1, buffer)
    second_pass(seq2, buffer)
    ... repeat ...

C++

对于 C++ 中的解决方案,我唯一能找到的就是用 C++ 协程模仿 yield,但我还没有找到任何关于如何做到这一点的好的参考资料。我也对这个问题的替代(非通用)解决方案感兴趣。我没有足够的内存预算来保留两次传递之间的序列副本。

【问题讨论】:

标签: c++ python generator yield coroutine


【解决方案1】:

可以通过简单的 goto 语句进行 yield 操作。因为很简单,所以我用C写的。

在你的生成器函数中你所要做的就是:

  • 所有变量都声明为静态变量
  • 最后的产量退出用标签记忆
  • 变量在函数结束时重新初始化

示例:

#include <stdio.h>

typedef struct {
    int i, j;
} Pair;

// the function generate_pairs  can generate values in successive calls.
// - all variables are declared as static
// - last yield exit is memorized with a label
// - variables are reinitialized at the end of function
Pair* generate_pairs(int imax, int jmax)
{
    // all local variable are declared static. So they are declared at the beginning
    static int i = 0;
    static int j = 0;
    static Pair p;
    // the exit position is marked with a label
    static enum {EBEGIN, EYIELD1} tag_goto = EBEGIN;
    
    // I goto to the last exit position
    if (tag_goto == EYIELD1)
        goto TYIELD1;
    
    
    for (i=0; i<imax; i++)   {
        for (j=0; j<jmax; j++)   {
            p.i = i;   p.j = -j;
            
            // I manage the yield comportment
            tag_goto = EYIELD1;
            return &p;
            TYIELD1 : ;
        }
        j = 0;
    }
    
    // reinitialization of variables
    i = 0;   j = 0;   // in fact this reinitialization is not useful in this example
    tag_goto = EBEGIN;
    
    // NULL means ends of generator
    return NULL; 
}

int main()
{
    for (Pair *p = generate_pairs(2,4); p != NULL; p = generate_pairs(2,4))
    {
        printf("%d,%d\n",p->i,p->j);
    }
    printf("end\n");
    return 0;
}

【讨论】:

    【解决方案2】:

    这个答案在 C 中有效(因此我认为在 C++ 中也有效)

    #include<stdint.h>
    //#include<stdio.h>
    
    #define MAX (1ll << 32) //2^32
    
    typedef struct {
        uint64_t i, j;
    } Pair;
    
    int generate_pairs(Pair* p)
    {
        static uint64_t i = 0;
        static uint64_t j = 0;
    
        p->i = i;
        p->j = j;
        
        if(++j == MAX)
        {
            j = 0;
            if(++i == MAX)
            {
                return -1; // return -1 to indicate generator finished.
            }
        }
        
        return 1; // return non -1 to indicate generator not finished.
    }
    
    int main()
    {
        while(1)
        {
            Pair p;
            int fin = generate_pairs(&p);
            
            //printf("%lld, %lld\n", p.i, p.j);
            
            if(fin == -1)
            {
                //printf("end");
                break;
            }
        }
        return 0;
    }
    

    这是模仿生成器的简单、非面向对象的方式。这对我来说按预期工作。

    编辑:之前的代码有误,我已经更新了。

    注意:此代码可以改进为仅使用 uint32_t 而不是给定问题的 uint64_t。

    【讨论】:

      【解决方案3】:

      在 C++ 中有迭代器,但实现迭代器并不简单:必须查阅 iterator concepts 并仔细设计新的迭代器类来实现它们。值得庆幸的是,Boost 有一个 iterator_facade 模板,它应该有助于实现迭代器和与迭代器兼容的生成器。

      有时a stackless coroutine can be used to implement an iterator

      附:另请参阅this article,其中提到了 Christopher M. Kohlhoff 的 switch hack 和 Oliver Kowalke 的 Boost.Coroutine。 Oliver Kowalke 在 Boost.Coroutine 上的工作 is a followup Giovanni P. Deretta。

      附:我觉得你也可以写一种生成器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;
      

      或者使用函子:

      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;
      

      附:这是一个使用 Mordor 协程实现的生成器:

      #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;
      }
      

      【讨论】:

        【解决方案4】:

        好吧,今天我也在寻找 C++11 下的简单集合实现。实际上我很失望,因为我发现的所有东西都与 python 生成器或 C# yield 运算符之类的东西太远了……或者太复杂了。

        目的是使集合仅在需要时才发出其项目。

        我希望它是这样的:

        auto emitter = on_range<int>(a, b).yield(
            [](int i) {
                 /* do something with i */
                 return i * 2;
            });
        

        我发现这篇文章,恕我直言,最佳答案是关于 boost.coroutine2,作者 Yongwei Wu。因为它最接近作者想要的。

        值得学习 boost couroutines .. 我可能会在周末做。但到目前为止,我正在使用我非常小的实现。希望对其他人有所帮助。

        下面是使用示例,然后是实现。

        Example.cpp

        #include <iostream>
        #include "Generator.h"
        int main() {
            typedef std::pair<int, int> res_t;
        
            auto emitter = Generator<res_t, int>::on_range(0, 3)
                .yield([](int i) {
                    return std::make_pair(i, i * i);
                });
        
            for (auto kv : emitter) {
                std::cout << kv.first << "^2 = " << kv.second << std::endl;
            }
        
            return 0;
        }
        

        Generator.h

        template<typename ResTy, typename IndexTy>
        struct yield_function{
            typedef std::function<ResTy(IndexTy)> type;
        };
        
        template<typename ResTy, typename IndexTy>
        class YieldConstIterator {
        public:
            typedef IndexTy index_t;
            typedef ResTy res_t;
            typedef typename yield_function<res_t, index_t>::type yield_function_t;
        
            typedef YieldConstIterator<ResTy, IndexTy> mytype_t;
            typedef ResTy value_type;
        
            YieldConstIterator(index_t index, yield_function_t yieldFunction) :
                    mIndex(index),
                    mYieldFunction(yieldFunction) {}
        
            mytype_t &operator++() {
                ++mIndex;
                return *this;
            }
        
            const value_type operator*() const {
                return mYieldFunction(mIndex);
            }
        
            bool operator!=(const mytype_t &r) const {
                return mIndex != r.mIndex;
            }
        
        protected:
        
            index_t mIndex;
            yield_function_t mYieldFunction;
        };
        
        template<typename ResTy, typename IndexTy>
        class YieldIterator : public YieldConstIterator<ResTy, IndexTy> {
        public:
        
            typedef YieldConstIterator<ResTy, IndexTy> parent_t;
        
            typedef IndexTy index_t;
            typedef ResTy res_t;
            typedef typename yield_function<res_t, index_t>::type yield_function_t;
            typedef ResTy value_type;
        
            YieldIterator(index_t index, yield_function_t yieldFunction) :
                    parent_t(index, yieldFunction) {}
        
            value_type operator*() {
                return parent_t::mYieldFunction(parent_t::mIndex);
            }
        };
        
        template<typename IndexTy>
        struct Range {
        public:
            typedef IndexTy index_t;
            typedef Range<IndexTy> mytype_t;
        
            index_t begin;
            index_t end;
        };
        
        template<typename ResTy, typename IndexTy>
        class GeneratorCollection {
        public:
        
            typedef Range<IndexTy> range_t;
        
            typedef IndexTy index_t;
            typedef ResTy res_t;
            typedef typename yield_function<res_t, index_t>::type yield_function_t;
            typedef YieldIterator<ResTy, IndexTy> iterator;
            typedef YieldConstIterator<ResTy, IndexTy> const_iterator;
        
            GeneratorCollection(range_t range, const yield_function_t &yieldF) :
                    mRange(range),
                    mYieldFunction(yieldF) {}
        
            iterator begin() {
                return iterator(mRange.begin, mYieldFunction);
            }
        
            iterator end() {
                return iterator(mRange.end, mYieldFunction);
            }
        
            const_iterator begin() const {
                return const_iterator(mRange.begin, mYieldFunction);
            }
        
            const_iterator end() const {
                return const_iterator(mRange.end, mYieldFunction);
            }
        
        private:
            range_t mRange;
            yield_function_t mYieldFunction;
        };
        
        template<typename ResTy, typename IndexTy>
        class Generator {
        public:
            typedef IndexTy index_t;
            typedef ResTy res_t;
            typedef typename yield_function<res_t, index_t>::type yield_function_t;
        
            typedef Generator<ResTy, IndexTy> mytype_t;
            typedef Range<IndexTy> parent_t;
            typedef GeneratorCollection<ResTy, IndexTy> finalized_emitter_t;
            typedef  Range<IndexTy> range_t;
        
        protected:
            Generator(range_t range) : mRange(range) {}
        public:
            static mytype_t on_range(index_t begin, index_t end) {
                return mytype_t({ begin, end });
            }
        
            finalized_emitter_t yield(yield_function_t f) {
                return finalized_emitter_t(mRange, f);
            }
        protected:
        
            range_t mRange;
        };      
        

        【讨论】:

          【解决方案5】:

          使用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;
          }
          

          【讨论】:

            【解决方案6】:

            类似this:

            使用示例:

            using ull = unsigned long long;
            
            auto main() -> int {
                for (ull val : range_t<ull>(100)) {
                    std::cout << val << std::endl;
                }
            
                return 0;
            }
            

            将打印从 0 到 99 的数字

            【讨论】:

              【解决方案7】:

              所有涉及编写自己的迭代器的答案都是完全错误的。这样的答案完全忽略了 Python 生成器(该语言最伟大和独特的功能之一)的意义。关于生成器最重要的事情是执行从中断的地方开始。迭代器不会发生这种情况。相反,您必须手动存储状态信息,以便当重新调用 operator++ 或 operator* 时,正确的信息会在下一个函数调用的最开始处到位。这就是为什么编写自己的 C++ 迭代器会非常痛苦的原因。而生成器很优雅,而且易于读写。

              我认为原生 C++ 中的 Python 生成器没有很好的模拟,至少目前还没有(有传言称yield will land in C++17)。您可以通过求助于第三方(例如 Yongwei 的 Boost 建议)或自己滚动获得类似的东西。

              我会说原生 C++ 中最接近的东西是线程。一个线程可以维护一组挂起的局部变量,并且可以在它停止的地方继续执行,这与生成器非常相似,但是您需要滚动一些额外的基础设施来支持生成器对象与其调用者之间的通信。例如

              // 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();
              }
              

              这个解决方案有几个缺点:

              1. 线程是“昂贵的”。大多数人会认为这是对线程的“过度”使用,尤其是当您的生成器如此简单时。
              2. 您需要记住几个清理操作。这些可以自动化,但您需要更多的基础设施,这可能再次被视为“过于奢侈”。无论如何,您需要的清理工作是:
                1. out->close()
                2. generator.join()
              3. 这不允许您停止生成器。您可以进行一些修改以添加该功能,但这会使代码变得混乱。它永远不会像 Python 的 yield 语句那样干净。
              4. 除了 2 之外,每次想要“实例化”生成器对象时,还需要其他一些样板文件:
                1. Channel* 输出参数
                2. main 中的附加变量:pairs、generator

              【讨论】:

              • 您将语法与功能混淆了。上面的一些答案实际上允许 C++ 从上次调用期间中断的地方继续执行。这没什么神奇的。事实上,Python用 C 实现的,所以 Python 中可能的任何东西在 C 中都是可能的,尽管没有那么方便。
              • @edy 在第一段中不是已经解决了吗?他并不是说不能在传统的 C++ 中创建等效的功能,只是说这是“巨大的痛苦”。
              • @Kaitain 这里的问题不是用 C++ 生成生成器是否痛苦,而是是否有一种模式可以这样做。他声称这种方法“没有抓住重点”,“最接近的东西”是线程......只是误导。是不是很痛?人们可以阅读其他答案并自行决定。
              • @edy 但是考虑到所有图灵完备的语言最终都具有相同的功能,这难道不是一个空洞吗? “X 中任何可能的东西在 Y 中都是可能的”对于所有此类语言都保证是正确的,但在我看来,这并不是一个很有启发性的观察。
              • @Kaitain 正是因为所有图灵完备的语言都应该具有相同的能力,因此如何用另一种语言实现一个功能的问题是合法的。 Python 没有什么是其他语言无法完成的;问题是效率和可维护性。在这两个方面,C++ 都是不错的选择。
              【解决方案8】:

              就像函数模拟堆栈的概念一样,生成器模拟队列的概念。剩下的就是语义。

              附带说明,您始终可以通过使用操作堆栈而不是数据来模拟带有堆栈的队列。这实际上意味着您可以通过返回一对来实现类似队列的行为,其中第二个值要么具有要调用的下一个函数,要么指示我们没有值。但这比收益与回报更普遍。它允许模拟任何值的队列,而不是您期望从生成器获得的同类值,但无需保留完整的内部队列。

              更具体地说,由于 C++ 没有对队列的自然抽象,因此您需要使用在内部实现队列的构造。因此,给出迭代器示例的答案是该概念的体面实现。

              这实际上意味着,如果您只想快速完成某些事情,然后使用队列的值,就像使用生成器产生的值一样,您可以使用简单的队列功能实现一些东西。

              【讨论】:

                【解决方案9】:

                生成器存在于 C++ 中,只是以另一个名称:输入迭代器。例如,从std::cin 读取类似于拥有char 的生成器。

                您只需要了解生成器的作用:

                • 有一团数据:局部变量定义了一个状态
                • 有一个init方法
                • 有一个“下一个”方法
                • 有一种方法可以发出终止信号

                在您的简单示例中,这很容易。从概念上讲:

                struct State { unsigned i, j; };
                
                State make();
                
                void next(State&);
                
                bool isDone(State const&);
                

                当然,我们把它包装成一个适当的类:

                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;
                };
                

                嗯嗯嗯...可能是 C++ 有点冗长:)

                【讨论】:

                • 我接受了您的回答(谢谢!),因为它在技术上对于我提出的问题是正确的。在需要生成的序列更复杂的情况下,您是否有任何技术指针,或者我只是在这里用 C++ 打死马,而协程真的是通用性的唯一方法?
                • @NoahWatkins:当语言支持协程时,协程可以轻松实现。不幸的是 C++ 没有,所以迭代更容易。如果你真的需要协程,你实际上需要一个完整的线程来保存你的函数调用的“堆栈”。在此示例中打开这样一罐蠕虫绝对是矫枉过正,但您的里程可能会根据您的实际需求而有所不同。
                • @boycy:实际上有多个协程提案,特别是一个无栈和另一个栈满。很难破解,所以现在我在等待。与此同时,无堆栈协程可以直接作为输入迭代器实现(只是,没有糖)。
                • 但类似的是,迭代器与生成器不同。
                • 如果你把它分成两个单独的 C++03 和 C++11 版本,这段代码会读起来更好......(或者干脆完全摆脱 C++03;人们应该'不要用它写新代码)
                【解决方案10】:

                由于Boost.Coroutine2现在支持得很好(我发现它是因为我想解决完全相同的yield问题),所以我贴出符合你初衷的C++代码:

                #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();
                    //}
                }
                

                在此示例中,pair_sequence 不采用其他参数。如果需要,当传递给coro_t::pull_type 构造函数时,应使用std::bind 或lambda 生成一个只接受一个参数(push_type)的函数对象。

                【讨论】:

                • 请注意,Coroutine2 需要 c++11,Visual Studio 2013 还不够,因为它只是部分支持。
                【解决方案11】:

                您可能应该在 Visual Studio 2015 的 std::experimental 中检查生成器,例如:https://blogs.msdn.microsoft.com/vcblog/2014/11/12/resumable-functions-in-c/

                我认为这正是您正在寻找的。整体生成器应该在 C++17 中可用,因为这只是 Microsoft VC 的实验性功能。

                【讨论】:

                • c++20 有协程,但没有提供生成器。 (但建议)您可以自己创建一个生成器。
                【解决方案12】:

                这样的事情非常相似:

                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;
                }
                

                使用 operator() 只是你想用这个生成器做什么的问题,你也可以将它构建为流并确保它适应 istream_iterator,例如。

                【讨论】:

                  【解决方案13】:

                  如果您只需要对相对较少的特定生成器执行此操作,则可以将每个生成器实现为一个类,其中成员数据相当于 Python 生成器函数的局部变量。然后你有一个 next 函数,它返回生成器将产生的下一个东西,更新内部状态。

                  我相信这基本上类似于 Python 生成器的实现方式。主要区别在于它们可以记住生成器函数的字节码中的偏移量作为“内部状态”的一部分,这意味着生成器可以写成包含产量的循环。您将不得不从前一个值计算下一个值。对于您的pair_sequence,这非常简单。它可能不适用于复杂的生成器。

                  您还需要某种方式来表示终止。如果您返回的是“指针式”,并且 NULL 不应是有效的可产生值,您可以使用 NULL 指针作为终止指示符。否则,您需要带外信号。

                  【讨论】:

                    猜你喜欢
                    • 2011-06-08
                    • 1970-01-01
                    • 2015-02-09
                    • 1970-01-01
                    • 1970-01-01
                    • 2021-03-18
                    • 1970-01-01
                    相关资源
                    最近更新 更多