同等のC ++からPythonジェネレーターへのパターン


117

C ++で模倣する必要があるPythonコードの例をいくつか持っています。特定のソリューション(コルーティンベースの歩留まりソリューションなど、それらは許容できる回答ですが)は必要ありません。何らかの方法でセマンティクスを再現する必要があるだけです。

パイソン

これは基本的なシーケンスジェネレーターであり、実体化されたバージョンを格納するには明らかに大きすぎます。

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

目標は、上記のシーケンスの2つのインスタンスを維持し、それらをセミロックステップでチャンクで反復処理することです。以下の例では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 ++のソリューションで見つけることができる唯一のことはyield、C ++コルーチンを模倣することですが、これを行う方法に関する適切なリファレンスは見つかりませんでした。この問題の代替(一般的ではない)ソリューションにも興味があります。パス間でシーケンスのコピーを保持するのに十分なメモリバジェットがありません。


ここからわかるように、stackoverflow.com / questions / 3864410 / … コルーチンを実装するのは良い考えではありません。バッファリングされた読み取りでそれを行うことはできませんか?stackoverflow.com/questions/4685862/...
batbaatar

C ++イテレータは、このようなものをサポートする必要があります。
ララランド2012年

回答:


79

ジェネレーターは、C ++では別の名前で存在します:Input Iterators。たとえば、からの読み取り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 ++は少し冗長なかもしれません:)


2
私はあなたの答えを受け入れました(ありがとう!)それは私が与えた質問に対して技術的に正しいので。生成する必要のあるシーケンスがより複雑である場合のテクニックへのポインタはありますか、またはここでC ++を使用して死んだ馬を打ち負かしていて、本当にコルーチンが一般性のための唯一の方法ですか?
Noah Watkins

3
@NoahWatkins:言語がサポートしている場合、コルーチンは実装を容易にします。残念ながらC ++はそうではないので、反復はより簡単です。コルーチンが本当に必要な場合は、関数呼び出しの「スタック」をサイドに保持する本格的なスレッドが実際に必要です。この例でこのようなワームの缶を開くことは間違いなくやり過ぎですが、実際のニーズに応じて走行距離は異なります。
Matthieu M.

1
非スレッドベースのコルーチンの実装は、Boost boost.org/doc/libs/1_57_0/libs/coroutine/doc/html/index.htmlで利用でき、ここで標準化の提案が行われています:open-std.org/jtc1/sc22/ wg21 / docs / papers / 2014 / n3985.pdf
15年

2
@boycy:実際にはコルーチンに対して複数の提案があり、特に1つはスタックレスで、もう1つはスタックフルです。割るのが難しいので、とりあえず待ちます。とりあえず、スタックのないコルーチンは、(糖分なしで)入力反復子としてほぼ直接実装できます。
Matthieu M.

3
イテレータはジェネレータと同じではありません。
ОгњенШобајић

52

C ++にはイテレータがありますが、イテレータの実装は簡単ではありません。イテレータの概念を調べ、新しいイテレータクラスを慎重に設計して実装する必要があります。ありがたいことに、Boostには、イテレーターおよびイテレーター互換ジェネレーターの実装に役立つiterator_facadeテンプレートがあります

時には、スタックレスコルーチンはイテレータを実装するために使用することができます

PSも参照してください この記事の両方に言及switchクリストファー・M. KohlhoffとによってハックをBoost.CoroutineオリバーKowalkeで。Oliver Kowalkeの仕事は、 Giovanni P. Deretta によるBoost.Coroutineのフォローアップです。

PS私はラムダを使って一種のジェネレータ書くこともできると思います:

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またはラムダを使用push_typeして、coro_t::pull_typeコンストラクタに渡されたときに(の)引数を1つだけ取る関数オブジェクトを生成する必要があります。


Coroutine2はc ++ 11を必要とすることに注意してください。これは、部分的にしかサポートされていないため、Visual Studio 2013では不十分です。
Weston

5

あなた自身のイテレータを書くことを含むすべての答えは完全に間違っています。そのような答えは、Pythonジェネレーター(言語の最大かつユニークな機能の1つ)の要点を完全に逃します。ジェネレータについて最も重要なことは、実行が中断したところから再開することです。これはイテレータには起こりません。代わりに、状態情報を手動で保存して、operator ++またはoperator *が新たに呼び出されたときに正しい情報が最初に配置れるようにする必要があります。次の関数呼び出しのする必要があります。これが、独自のC ++イテレーターを作成するのが非常に面倒な理由です。一方、ジェネレーターはエレガントで、読み取りと書き込みが簡単です。

私はネイティブC ++のPythonジェネレーターに良いアナログがあるとは思わない、少なくともまだ(利回りがC ++ 17に到達するという噂がある))。サードパーティ(Yongwei's 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. メインの追加変数:ペア、ジェネレーター

構文と機能が混同されています。上記のいくつかの回答により、C ++は最後の呼び出し中に中断されたところから実行をピックアップできます。それは不思議なことではありません。実際のところ、Python Cで実装されているため、それほど便利ではありませんが、Pythonで可能なことはすべてCでも可能です。
Edy

@edyそれは最初の段落ですでに扱われていませんか?彼は、同等の機能を従来のC ++では作成できないと主張しているのではなく、「巨大な痛み」だと主張しています。
Kaitain

@Kaitainここでの問題は、C ++でジェネレーターを実行するのが面倒かどうかではなく、そうするパターンがあるかどうかです。彼の主張は、アプローチが「ポイントを逃している」、「最も近いもの」はスレッドであるということです...誤解を招くだけです。痛いですか?他の答えを読んで、自分で決めることができます。
Edy

@edyしかし、すべてのチューリング完全言語が最終的に同じ機能を備えていることを考えると、これは空虚な点になりませんか?「Xで可能なことはすべてYでも可能である」というのは、そのようなすべての言語に当てはまることが保証されていますが、それは私には非常に明るい観察とは思えません。
カイテン

@Kaitain正確には、すべてのチューリング完全言語が同じ機能を持っているはずなので、ある機能を別の言語で実装する方法の問題は正当です。Pythonが持っていることは他の言語では実現できません。問題は効率と保守性です。どちらの点でも、C ++は良い(r)選択です。
Edy


2

比較的少数の特定のジェネレーターに対してのみこれを実行する必要がある場合は、それぞれをクラスとして実装できます。メンバーデータは、Pythonジェネレーター関数のローカル変数に相当します。次に、ジェネレーターが次に生成するものを返し、内部状態を更新する次の関数があります。

これは基本的にPythonジェネレーターの実装方法に似ていると思います。主な違いは、「内部状態」の一部としてジェネレーター関数のバイトコードへのオフセットを記憶できることです。これは、ジェネレーターがイールドを含むループとして記述できることを意味します。代わりに、前の値から次の値を計算する必要があります。あなたの場合pair_sequence、それはかなり簡単です。複雑なジェネレーター用ではない場合があります。

終了を示す何らかの方法も必要です。返すものが「ポインターのような」ものであり、NULLが有効な生成可能な値であってはならない場合は、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についてでした、この記事を見つけたYongwei呉。それは作者が望んでいたものに最も近いからです。

ブーストルーチンを学ぶ価値はあります。そして、おそらく週末に学習します。しかし、これまでのところ、非常に小さな実装を使用しています。それが他の誰かに役立つことを願っています。

以下は使用例、実装例です。

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

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

関数がスタックの概念をシミュレートするように、ジェネレーターはキューの概念をシミュレートします。残りはセマンティクスです。

補足として、データの代わりに操作のスタックを使用することにより、スタックを備えたキューを常にシミュレートできます。これが実際に意味することは、ペアを返すことでキューのような動作を実装できることです。2番目の値には、次に呼び出される関数があるか、値がないことを示します。しかし、これはイールド対リターンが行うことよりも一般的です。これにより、ジェネレーターから期待される同種の値ではなく、任意の値のキューをシミュレートできますが、完全な内部キューを維持する必要はありません。

具体的には、C ++にはキューの自然な抽象化がないため、内部でキューを実装する構成を使用する必要があります。したがって、反復子を使用した例を示した答えは、概念の適切な実装です。

これが実際に意味することは、ジェネレータから生成された値を消費するのと同じように、何かをすばやくしたい場合は、必要最小限のキュー機能で何かを実装し、キューの値を消費できるということです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.