Entspricht dem Muster des C ++ - Python-Generators

117

Ich habe einen Beispiel-Python-Code, den ich in C ++ nachahmen muss. Ich benötige keine spezifische Lösung (z. B. auf Co-Routine basierende Ertragslösungen, obwohl dies auch akzeptable Antworten wären). Ich muss lediglich die Semantik auf irgendeine Weise reproduzieren.

Python

Dies ist ein grundlegender Sequenzgenerator, der eindeutig zu groß ist, um eine materialisierte Version zu speichern.

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

Das Ziel besteht darin, zwei Instanzen der obigen Sequenz beizubehalten und sie im Semi-Lockstep, jedoch in Blöcken, zu durchlaufen. Im folgenden Beispiel first_passverwendet der die Sequenz von Paaren, um den Puffer zu initialisieren, und der second_passregeneriert dieselbe exakte Sequenz und verarbeitet den Puffer erneut.

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

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

C ++

Das einzige, was ich für eine Lösung in C ++ finden kann, ist die Nachahmung yieldmit C ++ - Coroutinen, aber ich habe keine gute Referenz dafür gefunden. Ich interessiere mich auch für alternative (nicht allgemeine) Lösungen für dieses Problem. Ich habe nicht genügend Speicherbudget, um eine Kopie der Sequenz zwischen den Durchläufen aufzubewahren.

Noah Watkins
quelle
Wie Sie hier sehen können, ist die Implementierung von stackoverflow.com/questions/3864410/… coroutine keine gute Idee. Kannst du es nicht mit gepuffertem Lesen machen? stackoverflow.com/questions/4685862/…
Batbaatar
C ++ - Iteratoren sollten so etwas unterstützen.
Lalaland
Verwandte: Äquivalent in C ++ der Ausbeute in C #?
Bernhard Barker

Antworten:

79

Generatoren existieren in C ++ unter einem anderen Namen: Input Iterators . Zum Beispiel std::cinähnelt das Lesen von einem Generator von char.

Sie müssen nur verstehen, was ein Generator tut:

  • Es gibt einen Datenklecks: Die lokalen Variablen definieren einen Zustand
  • Es gibt eine Init-Methode
  • Es gibt eine "nächste" Methode
  • Es gibt eine Möglichkeit, die Beendigung zu signalisieren

In Ihrem trivialen Beispiel ist es einfach genug. Konzeptionell:

struct State { unsigned i, j; };

State make();

void next(State&);

bool isDone(State const&);

Natürlich wickeln wir dies als eine richtige Klasse ein:

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

Also hum yeah ... könnte sein, dass C ++ ein bisschen ausführlicher ist :)

Matthieu M.
quelle
2
Ich habe Ihre Antwort akzeptiert (danke!), Weil sie für die von mir gestellte Frage technisch korrekt ist. Haben Sie Hinweise für Techniken in Fällen, in denen die zu generierende Sequenz komplexer ist, oder schlage ich hier nur ein totes Pferd mit C ++ und wirklich Coroutinen sind der einzige Weg für die Allgemeinheit?
Noah Watkins
3
@NoahWatkins: Coroutinen ermöglichen eine einfache Implementierung, wenn Sprachen sie unterstützen. Leider nicht in C ++, so dass die Iteration einfacher ist. Wenn Sie wirklich Coroutinen benötigen, benötigen Sie tatsächlich einen vollständigen Thread, um den "Stapel" Ihres Funktionsaufrufs auf der Seite zu halten. Es ist definitiv übertrieben, eine solche Dose Würmer nur dafür in diesem Beispiel zu öffnen , aber Ihr Kilometerstand kann je nach Ihren tatsächlichen Bedürfnissen variieren.
Matthieu M.
1
Eine nicht threadbasierte Coroutine-Implementierung finden Sie unter Boost boost.org/doc/libs/1_57_0/libs/coroutine/doc/html/index.html mit einem Vorschlag zur Standardisierung hier: open-std.org/jtc1/sc22/ wg21 / docs / papers / 2014 / n3985.pdf
Boycy
2
@boycy: Es gibt tatsächlich mehrere Vorschläge für Coroutinen, insbesondere einen ohne Stapel und den anderen mit Stapel. Es ist schwer zu knacken, also warte ich jetzt. In der Zwischenzeit können stapellose Coroutinen nahezu direkt als Input-Iteratoren implementiert werden (nur ohne Zucker).
Matthieu M.
3
Ähnlich sind Iteratoren jedoch nicht dasselbe wie Generatoren.
8гњен Шобајић
52

In C ++ gibt es Iteratoren, aber die Implementierung eines Iterators ist nicht einfach: Man muss die Iteratorkonzepte konsultieren und die neue Iteratorklasse sorgfältig entwerfen, um sie zu implementieren. Zum Glück verfügt Boost über eine iterator_facade- Vorlage, die bei der Implementierung der Iteratoren und iterator-kompatiblen Generatoren helfen soll.

Manchmal kann eine stapellose Coroutine verwendet werden, um einen Iterator zu implementieren .

PS Siehe auch diesen Artikel, in dem sowohl ein switchHack von Christopher M. Kohlhoff als auch Boost.Coroutine von Oliver Kowalke erwähnt werden. Oliver Kowalkes Arbeit ist eine Fortsetzung von Boost.Coroutine von Giovanni P. Deretta.

PS Ich denke, Sie können auch eine Art Generator mit Lambdas schreiben :

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;

Oder mit einem Funktor:

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 Hier ist ein Generator, der mit den Mordor- Coroutinen implementiert wurde :

#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;
}
ArtemGr
quelle
22

Da Boost.Coroutine2 es jetzt sehr gut unterstützt (ich habe es gefunden, weil ich genau das gleiche yieldProblem lösen wollte ), veröffentliche ich den C ++ - Code, der Ihrer ursprünglichen Absicht entspricht:

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

In diesem Beispiel werden pair_sequencekeine zusätzlichen Argumente verwendet. Wenn dies erforderlich ist std::bindoder ein Lambda verwendet werden sollte, um ein Funktionsobjekt zu generieren, das nur ein Argument (von push_type) akzeptiert , wenn es an den coro_t::pull_typeKonstruktor übergeben wird.

Yongwei Wu
quelle
Beachten Sie, dass für Coroutine2 c ++ 11 erforderlich ist, für das Visual Studio 2013 nicht ausreicht, da es nur teilweise unterstützt wird.
Weston
5

Alle Antworten, bei denen Sie einen eigenen Iterator schreiben, sind völlig falsch. Solche Antworten verfehlen völlig den Sinn der Python-Generatoren (eines der größten und einzigartigsten Merkmale der Sprache). Das Wichtigste an Generatoren ist, dass die Ausführung dort fortgesetzt wird, wo sie aufgehört hat. Dies passiert Iteratoren nicht. Stattdessen müssen Sie Statusinformationen manuell speichern, sodass beim erneuten Aufruf von Operator ++ oder Operator * die richtigen Informationen zu Beginn des nächsten Funktionsaufrufs vorhanden sind. Aus diesem Grund ist das Schreiben eines eigenen C ++ - Iterators ein gigantischer Schmerz. Generatoren sind elegant und leicht zu lesen und zu schreiben.

Ich glaube nicht, dass es ein gutes Analogon für Python-Generatoren in nativem C ++ gibt, zumindest noch nicht (es gibt ein Gerücht, dass die Ausbeute in C ++ 17 landen wird ). Sie können etwas Ähnliches erreichen, indem Sie auf Dritte zurückgreifen (z. B. Yongweis Boost-Vorschlag) oder Ihre eigenen rollen.

Ich würde sagen, das Nächste in nativem C ++ sind Threads. Ein Thread kann einen angehaltenen Satz lokaler Variablen verwalten und die Ausführung dort fortsetzen, wo er aufgehört hat, ähnlich wie bei Generatoren. Sie müssen jedoch ein wenig zusätzliche Infrastruktur bereitstellen, um die Kommunikation zwischen dem Generatorobjekt und seinem Aufrufer zu unterstützen. Z.B

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

Diese Lösung hat jedoch mehrere Nachteile:

  1. Threads sind "teuer". Die meisten Leute würden dies als "extravagante" Verwendung von Threads betrachten, insbesondere wenn Ihr Generator so einfach ist.
  2. Es gibt einige Bereinigungsaktionen, an die Sie sich erinnern müssen. Diese könnten automatisiert werden, aber Sie würden noch mehr Infrastruktur benötigen, was wiederum wahrscheinlich als "zu extravagant" angesehen wird. Wie auch immer, die Aufräumarbeiten, die Sie brauchen, sind:
    1. out-> close ()
    2. generator.join ()
  3. Dadurch können Sie den Generator nicht stoppen. Sie können einige Änderungen vornehmen, um diese Fähigkeit hinzuzufügen, aber der Code wird dadurch unübersichtlicher. Es wäre niemals so sauber wie Pythons Ertragsaussage.
  4. Zusätzlich zu 2 gibt es weitere Teile der Boilerplate, die jedes Mal benötigt werden, wenn Sie ein Generatorobjekt "instanziieren" möchten:
    1. Kanal * out-Parameter
    2. Zusätzliche Variablen in main: Paare, Generator
allyourcode
quelle
Sie verwechseln Syntax mit Funktionalität. Ein paar Antworten oben ermöglichen es dem C ++, die Ausführung dort aufzunehmen, wo sie beim letzten Aufruf aufgehört hat. Es ist nichts Magisches. Wie in der Tat, Python ist in C implementiert, so was in Python ist möglich , in C möglich ist, wenn auch nicht so bequem.
Edy
@edy Wird das nicht schon im ersten Absatz angesprochen? Er behauptet nicht, dass in herkömmlichem C ++ keine gleichwertige Funktionalität erstellt werden kann, sondern nur, dass dies „ein gigantischer Schmerz“ ist.
Kaitain
@Kaitain Die Frage hier ist nicht, ob es schwierig ist, einen Generator in C ++ zu erstellen, sondern ob es ein Muster dafür gibt. Seine Behauptungen, dass der Ansatz "Miss the Point", dass das "Nächste" Fäden sind ... sind nur irreführend. Ist es ein Schmerz? Man konnte die anderen Antworten durchlesen und selbst entscheiden.
Edy
@edy Aber ist dies nicht ein leerer Punkt, da alle Turing-vollständigen Sprachen letztendlich dieselbe Funktionalität bieten können? "Was in X möglich ist, ist in Y möglich" gilt garantiert für alle diese Sprachen, aber das scheint mir keine sehr aufschlussreiche Beobachtung zu sein.
Kaitain
@Kaitain Gerade weil angeblich alle Turing-vollständigen Sprachen die gleichen Fähigkeiten haben sollten, ist die Frage, wie ein Feature in einer anderen Sprache implementiert werden kann, legitim. Nichts, was Python hat, kann nicht von einer anderen Sprache erreicht werden. Die Frage ist Effizienz und Wartbarkeit. In beiden Fällen wäre C ++ eine gute Wahl.
Edy
2

Wenn Sie dies nur für eine relativ kleine Anzahl spezifischer Generatoren tun müssen, können Sie jeden als Klasse implementieren, wobei die Mitgliedsdaten den lokalen Variablen der Python-Generatorfunktion entsprechen. Dann haben Sie eine nächste Funktion, die das nächste Ergebnis des Generators zurückgibt und dabei den internen Status aktualisiert.

Dies ähnelt im Grunde der Implementierung von Python-Generatoren, glaube ich. Der Hauptunterschied besteht darin, dass sie sich einen Teil des Bytecodes für die Generatorfunktion als Teil des "internen Zustands" merken können, was bedeutet, dass die Generatoren als Schleifen geschrieben werden können, die Ausbeuten enthalten. Sie müssten stattdessen den nächsten Wert aus dem vorherigen berechnen. Bei Ihnen pair_sequenceist das ziemlich trivial. Es ist möglicherweise nicht für komplexe Generatoren.

Sie benötigen auch eine Möglichkeit, die Beendigung anzuzeigen. Wenn das, was Sie zurückgeben, "zeigerartig" ist und NULL kein gültiger Ertragswert sein sollte, können Sie einen NULL-Zeiger als Beendigungsindikator verwenden. Andernfalls benötigen Sie ein Außerbandsignal.

Ben
quelle
1

So etwas ist sehr ähnlich:

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

Die Verwendung des Operators () ist nur eine Frage dessen, was Sie mit diesem Generator tun möchten. Sie können ihn auch als Stream erstellen und sicherstellen, dass er sich beispielsweise an einen istream_iterator anpasst.

Lippe
quelle
1

Verwenden von 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;
}
Ingenieur
quelle
0

So etwas wie das :

Anwendungsbeispiel:

using ull = unsigned long long;

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

    return 0;
}

Druckt die Zahlen von 0 bis 99

smac89
quelle
0

Nun, heute suchte ich auch nach einer einfachen Implementierung der Sammlung unter C ++ 11. Eigentlich war ich enttäuscht, weil alles, was ich gefunden habe, zu weit von Dingen wie Python-Generatoren oder C # -Ertragsoperatoren entfernt ist ... oder zu kompliziert.

Der Zweck besteht darin, eine Sammlung zu erstellen, die ihre Gegenstände nur dann ausgibt, wenn dies erforderlich ist.

Ich wollte, dass es so ist:

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

Ich fand diesen Beitrag, IMHO beste Antwort war über boost.coroutine2, von Yongwei Wu . Da es dem Autor am nächsten kommt.

Es lohnt sich, Boost Couroutinen zu lernen. Und ich werde es vielleicht am Wochenende tun. Bisher verwende ich jedoch meine sehr kleine Implementierung. Hoffe es hilft jemand anderem.

Unten finden Sie ein Beispiel für die Verwendung und anschließende Implementierung.

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;
};      
Stepan Dyatkovskiy
quelle
0

Diese Antwort funktioniert in C (und daher denke ich, funktioniert auch in 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;
}

Dies ist eine einfache, nicht objektorientierte Methode, um einen Generator nachzuahmen. Das hat bei mir wie erwartet funktioniert.

Sourav Kannantha B.
quelle
-1

So wie eine Funktion das Konzept eines Stapels simuliert, simulieren Generatoren das Konzept einer Warteschlange. Der Rest ist Semantik.

Nebenbei bemerkt, Sie können eine Warteschlange immer mit einem Stapel simulieren, indem Sie einen Stapel von Operationen anstelle von Daten verwenden. Praktisch bedeutet dies, dass Sie ein warteschlangenähnliches Verhalten implementieren können, indem Sie ein Paar zurückgeben, dessen zweiter Wert entweder die nächste aufzurufende Funktion hat oder anzeigt, dass wir keine Werte mehr haben. Dies ist jedoch allgemeiner als das, was Rendite gegen Rendite bewirkt. Es ermöglicht die Simulation einer Warteschlange mit beliebigen Werten anstelle von homogenen Werten, die Sie von einem Generator erwarten, ohne jedoch eine vollständige interne Warteschlange beizubehalten.

Da C ++ keine natürliche Abstraktion für eine Warteschlange hat, müssen Sie Konstrukte verwenden, die eine Warteschlange intern implementieren. Die Antwort, die das Beispiel mit Iteratoren gab, ist also eine anständige Umsetzung des Konzepts.

Praktisch bedeutet dies, dass Sie etwas mit Bare-Bones-Warteschlangenfunktion implementieren können, wenn Sie nur etwas schnelles möchten und dann die Werte der Warteschlange genauso verbrauchen, wie Sie die Werte eines Generators verbrauchen würden.

Dmitry Rubanovich
quelle