等价的C ++到Python生成器模式

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

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 ++中我唯一可以find的解决scheme是用C ++协程来模仿yield ,但是我没有find关于如何做这个的好的参考。 我也对这个问题的替代(非一般)解决scheme感兴趣。 我没有足够的内存预算来保留传递之间的序列副本。

生成器以C ++存在,就在另一个名字下: Input Iterators 。 例如,从std::cin读取类似于具有char的生成器。

您只需要了解发电机的function:

  • 有一块数据:局部variables定义一个状态
  • 有一个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 ++中有迭代器,但是实现迭代器并不简单:必须查阅迭代器概念并仔细devise新的迭代器类来实现它们。 值得庆幸的是,Boost有一个iterator_facade模板,可以帮助实现迭代器和迭代器兼容的生成器。

有时可以使用无堆栈协程来实现迭代器 。

PS另请参阅这篇文章 ,其中提到了克里斯托弗·M·科尔霍夫(Christopher M. Kohlhoff)和奥利弗·科瓦尔克(Oliver Kowalke)的Boost.Coroutine 。 Oliver Kowalke的作品是 Giovanni P. Deretta的Boost.Coroutine 的后续作品。

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

PS这是一个用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; } 

由于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应该被用来生成一个只有一个参数( push_type )的函数对象,当它被传递给coro_t::pull_type构造函数时。

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

我想这正是你在找什么。 整体生成器应该在C ++ 17中可用,因为这只是实验性的Microsoft VCfunction。

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

我相信这与Python生成器的实现基本相似。 主要区别在于它们可以记住发生器函数字节码的偏移,作为“内部状态”的一部分,这意味着生成器可以写成包含yield的循环。 你将不得不计算从前一个值。 在你的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()只是一个你想用这个生成器做什么的问题,你也可以把它构build成一个stream,并确保它适应istream_iterator。

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

作为一个方面说明,你总是可以通过使用一堆操作而不是数据来模拟一个堆栈的队列。 这实际上意味着你可以通过返回一个对来实现一个类似队列的行为,其中第二个值具有要调用的下一个函数或者表示我们超出了值。 但是这比收益率和回报率更一般。 它允许模拟任何值的队列,而不是您期望从生成器获得的同类值,但不保留完整的内部队列。

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

这实际上意味着你可以实现一些基本function,如果你只需要一些快速的东西,然后消耗队列的值就像使用发生器产生的值一样。

所有涉及编写自己的迭代器的答案都是完全错误的。 这样的答案完全错过了Python生成器(语言的最大和独特的function之一)的点。 关于发电机最重要的是执行从停止的地方。 这不会发生迭代器。 相反,您必须手动存储状态信息,以便在重新调用operator ++或operator *时,正确的信息在下一个函数调用的最开始处就位。 这就是为什么编写你自己的C ++迭代器是一个巨大的痛苦; 而生成器是优雅的,并且易于读取和写入。

我不认为Python本地C ++中有一个很好的模拟器,至less现在还没有(有一个代号是C ++ 17 )。 你可以通过诉诸第三方(例如Yongwei的Boostbuild议)来获得类似的东西,或者自己动手。

我会说本地C ++最接近的是线程。 一个线程可以维护一个暂停的局部variables集合,并且可以继续执行,就像发生器一样,但是你需要滚动一些额外的基础结构来支持生成器对象和调用者之间的通信。 例如

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

这个解决scheme有几个缺点:

  1. 线程是“昂贵的”。 大多数人会认为这是一个“奢侈”的线程使用,特别是当你的发电机是如此简单。
  2. 有几个清理行动,你需要记住。 这些可以是自动化的,但是你需要更多的基础设施,而这又可能被视为“太奢侈”了。 无论如何,你需要清理的是:
    1. OUT-> close()方法
    2. generator.join()
  3. 这不允许你停止发电机。 你可以做一些修改来增加这个能力,但是会给代码添加混乱。 它永远不会像Python的yield语句那么干净。
  4. 除了2之外,还有其他一些样板文件,每次需要“实例化”一个生成器对象时都需要:
    1. 频道*出参数
    2. 主要的其他variables:对,发生器

像这样的东西:

使用示例:

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

将打印从0到99的数字