it-swarm.com.de

Entspricht dem C++ - Python-Generatormuster

Ich habe ein Beispiel für Python-Code, den ich in C++ nachahmen muss. Ich benötige keine spezifische Lösung (wie z. B. Co-Routine-basierte Ertragslösungen, auch wenn sie akzeptable Antworten wären), ich muss einfach 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 ist es, zwei Instanzen der obigen Sequenz beizubehalten und sie im Halb-Lockstep, aber in Abschnitten zu durchlaufen. Im folgenden Beispiel verwendet first_pass die Sequenz von Paaren, um den Puffer zu initialisieren, und second_pass generiert die gleiche 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, yield mit C++ - Coroutinen nachzuahmen, aber ich habe keine gute Referenz gefunden, wie man das macht. 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 Durchgängen aufzubewahren.

86
Noah Watkins

Generatoren gibt es in C++ nur unter einem anderen Namen: Input Iterators . Das Lesen von std::cin ähnelt beispielsweise dem Generieren von char.

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

  • es gibt einen Datenblock: Die lokalen Variablen definieren einen state
  • es gibt eine Init-Methode
  • es gibt eine "nächste" Methode
  • es gibt eine Möglichkeit, die Terminierung zu signalisieren

In Ihrem trivialen Beispiel ist das einfach genug. Konzeptionell:

struct State { unsigned i, j; };

State make();

void next(State&);

bool isDone(State const&);

Natürlich wickeln wir dies als 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;
};

Hmmm ja ... könnte sein, dass C++ etwas wortreicher ist :)

58
Matthieu M.

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 Vorlage iterator_facade , die die Implementierung der Iteratoren und Iterator-kompatiblen Generatoren unterstützen soll.

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

P.S. Siehe auch diesen Artikel , der sowohl einen switch-Hack von Christopher M. Kohlhoff als auch Boost.Coroutine von Oliver Kowalke erwähnt. Oliver Kowalkes Arbeit ist ein Nachfolger on Boost.Coroutine von Giovanni P. Deretta.

P.S. Ich denke, Sie können auch eine Art Generator schreiben mit 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;

Oder mit einem functor:

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;

P.S. Hier ist ein Generator, der mit den Mordor Coroutines 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;
}
39
ArtemGr

Da Boost.Coroutine2 es jetzt sehr gut unterstützt (ich fand es, weil ich genau das gleiche yield-Problem lösen wollte), poste 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 nimmt pair_sequence keine zusätzlichen Argumente an. Wenn dies erforderlich ist, sollte std::bind oder ein Lambda verwendet werden, um ein Funktionsobjekt zu generieren, das nur ein Argument (von Push_type) benötigt, wenn es an den coro_t::pull_type-Konstruktor übergeben wird.

19
Yongwei Wu

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

Ich glaube nicht, dass es ein gutes Analogon für Python-Generatoren in nativem C++ gibt, zumindest noch nicht (es gibt ein Problem, dass Yield in C++ 17 landet). Sie können etwas Ähnliches erhalten, indem Sie auf einen Drittanbieter zurückgreifen (z. B. den Boost-Vorschlag von Yongwei) oder einen eigenen Wurf durchführen.

Ich würde sagen, das nächste in native C++ ist Threads. Ein Thread kann einen ausgesetzten Satz lokaler Variablen verwalten und die Ausführung dort fortsetzen, wo er aufgehört hat, ähnlich wie Generatoren. Sie müssen jedoch ein wenig zusätzliche Infrastruktur einsetzen, 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 einige Nachteile:

  1. Fäden sind "teuer". Die meisten Leute würden dies für eine "extravagante" Verwendung von Threads halten, besonders wenn Ihr Generator so einfach ist.
  2. Es gibt einige Aufräumaktionen, an die Sie sich erinnern müssen. Diese könnten automatisiert werden, aber Sie würden noch mehr Infrastruktur benötigen, was wiederum als "zu extravagant" empfunden 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 Modifikationen vornehmen, um diese Fähigkeit hinzuzufügen, aber dies fügt dem Code Unordnung hinzu. Es wäre niemals so sauber wie die Ertragsaussage von Python.
  4. Zusätzlich zu 2 gibt es noch andere Boilerplate-Bits, die jedes Mal benötigt werden, wenn Sie ein Generatorobjekt "instanziieren" wollen:
    1. Channel * Out-Parameter
    2. Zusätzliche Variablen in main: Paare, Generator
3
allyourcode

Sie sollten wahrscheinlich Generatoren in std :: experimental in Visual Studio 2015 prüfen, z. B. https://blogs.msdn.Microsoft.com/vcblog/2014/11/12/resumable-functions-in-c/ .

Ich denke es ist genau das, wonach du suchst. Generatoren sollten in C++ 17 verfügbar sein, da dies nur eine experimentelle Funktion von Microsoft VC ist.

Wenn Sie dies nur für eine relativ kleine Anzahl bestimmter Generatoren tun müssen, können Sie jeden als Klasse implementieren, wobei die Member-Daten den lokalen Variablen der Python-Generatorfunktion entsprechen. Dann haben Sie eine nächste Funktion, die das nächste gibt, was der Generator nachgeben würde, und aktualisiert dabei den internen Zustand.

Dies ist im Grunde ähnlich wie bei der Implementierung von Python-Generatoren, glaube ich. Der Hauptunterschied besteht darin, dass sie sich einen Versatz in den Bytecode für die Generatorfunktion als Teil des "internen Zustands" merken können, was bedeutet, dass die Generatoren als Schleifen geschrieben werden können, die Erträge enthalten. Sie müssten stattdessen den nächsten Wert vom vorherigen berechnen. Im Fall Ihres pair_sequence ist das ziemlich trivial. Es kann nicht für komplexe Generatoren sein.

Sie benötigen auch eine Möglichkeit, eine Kündigung anzuzeigen. Wenn das, was Sie zurückgeben, "zeigerartig" ist und NULL kein gültiger ausbaufähiger Wert sein sollte, könnten Sie einen NULL-Zeiger als Beendigungsindikator verwenden. Ansonsten benötigen Sie ein Out-of-Band-Signal.

2
Ben

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, was Sie mit diesem Generator machen möchten. Sie können ihn auch als Stream erstellen und sicherstellen, dass er beispielsweise an einen istream_iterator angepasst wird.

1
lip

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

So etwas wie this :

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

0
smac89

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

Der Zweck besteht darin, eine Sammlung zu erstellen, die ihre Artikel nur dann ausstrahlt, wenn sie benötigt wird.

Ich wollte es so sein:

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 ist es dem Autoren am nächsten.

Es lohnt sich, Couroutines zu lernen. Und ich mache es vielleicht am Wochenende. Aber bisher verwende ich meine sehr kleine Implementierung. Hoffe, es hilft jemand anderem.

Nachfolgend finden Sie ein Anwendungsbeispiel und anschließend die 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;
};      
0