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.
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 ...
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.
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:
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 :)
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;
}
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.
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:
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.
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.
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;
}
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
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;
};