与Python生成器模式等效的C ++


117

我有一些需要在C ++中模仿的示例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 ++解决方案,我唯一能找到的就是模仿yieldC ++协程,但是我还没有找到有关如何执行此操作的良好参考。我也对解决此问题的替代(非常规)解决方案感兴趣。我没有足够的内存预算来保留两次通过之间的序列副本。


从这里可以看到stackoverflow.com/questions/3864410/…协程不是一个好主意。你不能用缓冲阅读吗?stackoverflow.com/questions/4685862/...
batbaatar

C ++迭代器应该支持这样的东西。
拉拉兰(Lalaland)2012年

Answers:


79

生成器在C ++中存在,只是另外一个名称:Input Iterators。例如,从读取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 ++有点冗长:)


2
我接受了您的回答(谢谢!),因为它在技术上对我提出的问题是正确的。在需要生成的序列更复杂的情况下,您是否对技术有任何指示?或者我只是在这里用C ++击败老手,真正的协程才是通用性的唯一方法?
诺亚·沃特金斯

3
@NoahWatkins:当语言支持协程时,协程可以轻松实现。不幸的是C ++不能,所以迭代更容易。如果确实需要协程,则实际上需要一个成熟的线程来将函数调用的“堆栈”放在一边。在此示例中,仅为此打开一个蠕虫蠕虫罐头肯定是过头的,但实际里程可能会因实际需要而异。
Matthieu M.


2
@boycy:协程实际上有多种建议,特别是一个少堆栈的建议和另一个满堆栈的建议。很难破解,所以现在我在等待。但是,与此同时,无栈协程可以作为输入迭代器直接实现(只是没有糖)。
Matthieu M.

3
同样,迭代器与生成器并不相同。
ОгњенШобајић

52

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

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

PS另请参见 本文,其中同时提到了switchChristopher 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;
}

22

由于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构造函数时,应使用lambda生成仅包含(of )个参数的函数对象。


请注意,Coroutine2需要使用c ++ 11,对于Visual Studio 2013来说,由于仅部分受支持,因此Visual Studio 2013不足。
韦斯顿

5

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

我认为本机C ++中没有适合Python生成器的良好模拟,至少目前还没有(有传言称yield将在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. 主变量:对,生成器

您将语法与功能混为一谈。上面的一些答案实际上允许C ++从上次调用中中断的地方开始执行。没什么神奇的。实际上,Python 用C实现的,因此尽管Python不太方便,但Python可能实现的一切都是C。
Edy

@edy在第一段中是否已经解决了?他并没有声称不能在常规C ++中创建等效功能,只是说这是“巨大的痛苦”。
Kaitain

@Kaitain这里的问题不是用C ++生成生成器是否很麻烦,而是是否有一种模式可以这样做。他声称这种方法“错失了重点”,“最接近的东西”是线程……只是误导。痛苦吗?一个人可以通读其他答案,然后自己决定。
Edy

@edy但是,鉴于所有图灵完备的语言最终都具有相同的功能,这难道不是一个空谈点吗?对于所有这样的语言,“在X中可能发生的一切在Y中可能发生的一切”都保证是正确的,但是在我看来这并不是一个很有启发性的观察。
Kaitain

@Kaitain正是因为所有图灵完备的语言都应该具有相同的功能,所以如何用另一种语言实现一种功能的问题是合理的。Python不能用其他语言来完成;问题是效率和可维护性。在这两个方面,C ++都是不错的选择。
Edy


2

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

我相信,这基本上与Python生成器的实现方式相似。主要的区别是它们可以记住生成器函数的字节码中的偏移量,作为“内部状态”的一部分,这意味着可以将生成器写为包含yield的循环。您将不得不从上一个计算下一个值。在您的情况下pair_sequence,这是微不足道的。它可能不适用于复杂的发电机。

您还需要一些指示终止的方法。如果返回的是“类似指针的”,并且NULL不应为有效的yieldable值,则可以将NULL指针用作终止指示符。否则,您需要带外信号。


1

这样的事情非常相似:

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。


1

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

这样的东西:

使用示例:

using ull = unsigned long long;

auto main() -> int {
    for (ull val : range_t<ull>(100)) {
        std::cout << val << std::endl;
    }

    return 0;
}

将打印从0到99的数字


0

好吧,今天我也在寻找在C ++ 11下实现轻松收集的实现。实际上,我很失望,因为我发现的一切都与python生成器或C#yield操作符等东西相距太远……或者过于复杂。

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

我希望它像这样:

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

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

0

这个答案适用于C语言(因此我认为也适用于C ++语言)

#include <stdio.h>

const uint64_t MAX = 1ll<<32;

typedef struct {
    uint64_t i, j;
} Pair;

Pair* generate_pairs()
{
    static uint64_t i = 0;
    static uint64_t j = 0;
    
    Pair p = {i,j};
    if(j++ < MAX)
    {
        return &p;
    }
        else if(++i < MAX)
    {
        p.i++;
        p.j = 0;
        j = 0;
        return &p;
    }
    else
    {
        return NULL;
    }
}

int main()
{
    while(1)
    {
        Pair *p = generate_pairs();
        if(p != NULL)
        {
            //printf("%d,%d\n",p->i,p->j);
        }
        else
        {
            //printf("end");
            break;
        }
    }
    return 0;
}

这是一种模拟生成器的简单,非面向对象的方法。这按我的预期工作。


-1

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

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

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

这实际上意味着,如果您只想快速地进行操作,然后使用队列值,就可以使用生成器产生的值,那么您可以使用准系统队列功能来实现某些功能。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.