等效于C++ Python 生成器模式

Equivalent C++ to Python generator pattern

本文关键字:模式 Python C++      更新时间:2023-10-16

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

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

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++中,只是在另一个名称下:输入迭代器。例如,从std::cin读取类似于拥有 char 生成器。

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

  • 有一个数据斑点:局部变量定义一个状态
  • 有一个初始化方法
  • 有一个"下一个"方法
  • 有一种方法可以发出信号终止

在你的琐碎例子中,这很容易。概念:

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++中,有迭代器,但实现迭代器并不简单:必须参考迭代器概念并仔细设计新的迭代器类来实现它们。值得庆幸的是,Boost 有一个iterator_facade模板,它应该有助于实现迭代器和迭代器兼容的生成器。

有时,无堆栈协程可用于实现迭代器。

附言另请参阅这篇文章,其中提到了Christopher M. Kohlhoff的switch黑客和Oliver Kowalke的Boost.Coroutine。Oliver Kowalke的工作是Giovanni P. Deretta的Boost.Coroutine的后续作品。

附言我认为你也可以用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;

附言下面是使用魔多协程实现的生成器:

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

由于 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不接受其他参数。如果需要,std::bind或lambda应该用于生成一个函数对象,当它传递给coro_t::pull_type构造函数时,该对象仅接受一个参数(push_type(。

所有涉及编写自己的迭代器的答案都是完全错误的。这样的答案完全忽略了Python生成器(该语言最伟大和独特的功能之一(的重点。生成器最重要的一点是,执行从停止的地方开始。这不会发生在迭代器上。相反,您必须手动存储状态信息,以便在重新调用运算符 ++ 或运算符* 时,在下一个函数调用的最开始时会保留正确的信息。这就是为什么编写自己的C++迭代器是一个巨大的痛苦;而生成器很优雅,易于阅读+写入。

我认为原生C++中没有很好的 Python 生成器模拟,至少现在还没有(有传言说产量将在 C++17 年登陆(。您可以通过求助于第三方(例如永伟的 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. 通道* 输出参数
    2. 主变量中的附加变量:对,生成器

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

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

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

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

我相信,这基本上类似于Python生成器的实现方式。主要区别在于它们可以记住生成器函数的字节码偏移量作为"内部状态"的一部分,这意味着生成器可以写成包含产量的循环。您必须改为计算前一个值的下一个值。就您的pair_sequence而言,这是微不足道的。它可能不适用于复杂的发电机。

您还需要某种指示终止的方法。如果要返回的是"类似指针"的,并且 NULL 不应是有效的可生成值,则可以使用 NULL 指针作为终止指示器。否则,您需要带外信号。

像这样的东西非常相似:

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。

好吧,今天我也在寻找在 C++11 下轻松实现集合。实际上我很失望,因为我发现的所有内容都与 python 生成器或 C# 收益运算符等东西相去甚远......或者太复杂了。

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

我希望它是这样的:

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

找到了这篇文章,恕我直言,最好的答案是关于 boost.coroutine2,由吴永伟撰写。因为它最接近作者想要的。

值得学习的是助推表例程。我也许会在周末做。但到目前为止,我使用的是我的非常小的实现。希望对其他人有帮助。

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

示例.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;
}

发电机.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;
};      

这个答案在 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, %lldn", p.i, p.j);
        
        if(fin == -1)
        {
            //printf("end");
            break;
        }
    }
    return 0;
}

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

编辑:以前的代码是错误的,我已经更新了它。

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

可以通过

简单的 goto 语句获得屈服。因为它很简单,我用 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,%dn",p->i,p->j);
    }
    printf("endn");
    return 0;
}

像这样:

使用示例:

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

将打印从 0 到 99 的数字

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

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

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

这实际上意味着,如果您只是想要快速的东西,则可以使用基本队列功能实现某些内容,然后像使用生成器生成的值一样使用队列的值。