C ++ equivalente al pattern del generatore Python


117

Ho qualche esempio di codice Python che devo imitare in C ++. Non ho bisogno di alcuna soluzione specifica (come soluzioni di rendimento basate sulla co-routine, sebbene sarebbero anche risposte accettabili), ho semplicemente bisogno di riprodurre la semantica in qualche modo.

Pitone

Questo è un generatore di sequenze di base, chiaramente troppo grande per memorizzare una versione materializzata.

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

L'obiettivo è mantenere due istanze della sequenza di cui sopra e iterare su di esse in semi-lockstep, ma in blocchi. Nell'esempio seguente first_passutilizza la sequenza di coppie per inizializzare il buffer e second_passrigenera la stessa esatta sequenza ed elabora nuovamente il buffer.

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

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

C ++

L'unica cosa che posso trovare per una soluzione in C ++ è imitare yieldcon le coroutine C ++, ma non ho trovato alcun buon riferimento su come farlo. Sono anche interessato a soluzioni alternative (non generali) per questo problema. Non ho un budget di memoria sufficiente per conservare una copia della sequenza tra i passaggi.


Come puoi vedere da qui stackoverflow.com/questions/3864410/… coroutine non è una buona idea da implementare. Non puoi farlo con la lettura bufferizzata? stackoverflow.com/questions/4685862/...
batbaatar

Gli iteratori C ++ dovrebbero supportare qualcosa di simile.
Lalaland

Risposte:


79

I generatori esistono in C ++, appena sotto un altro nome: Input Iterators . Ad esempio, leggere da std::cinè simile ad avere un generatore dichar .

Devi semplicemente capire cosa fa un generatore:

  • c'è un blob di dati: le variabili locali definiscono un file stato
  • c'è un metodo init
  • esiste un metodo "successivo"
  • c'è un modo per segnalare la terminazione

Nel tuo banale esempio, è abbastanza facile. concettualmente:

struct State { unsigned i, j; };

State make();

void next(State&);

bool isDone(State const&);

Naturalmente, lo racchiudiamo come una classe appropriata:

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

Quindi hum yeah ... potrebbe essere che C ++ sia un po 'più prolisso :)


2
Ho accettato la tua risposta (grazie!) Perché è tecnicamente corretta per la domanda che ho dato. Hai qualche suggerimento per le tecniche nei casi in cui la sequenza che deve essere generata è più complessa, o sto solo battendo un cavallo morto qui con C ++ e davvero le coroutine sono l'unico modo per la generalità?
Noah Watkins,

3
@NoahWatkins: le coroutine facilitano l'implementazione quando le lingue le supportano. Sfortunatamente il C ++ non lo fa, quindi l'iterazione è più semplice. Se hai davvero bisogno di coroutine, in realtà hai bisogno di un thread completo per contenere lo "stack" della tua chiamata di funzione sul lato. È decisamente eccessivo aprire una tale lattina di worm solo per questo in questo esempio, ma il tuo chilometraggio può variare a seconda delle tue reali esigenze.
Matthieu M.

1
Un'implementazione coroutine non basata su thread è disponibile in Boost boost.org/doc/libs/1_57_0/libs/coroutine/doc/html/index.html con una proposta di standardizzazione qui: open-std.org/jtc1/sc22/ wg21 / docs / papers / 2014 / n3985.pdf
boicottaggio

2
@boycy: Ci sono in realtà più proposte per le coroutine, in particolare una senza stack e l'altra piena. È difficile da risolvere, quindi per ora sto aspettando. Nel frattempo, però, le coroutine senza stack sono implementabili direttamente come Iteratori di input (solo, senza lo zucchero).
Matthieu M.

3
Tuttavia, simili, gli iteratori non sono gli stessi dei generatori.
Огњен Шобајић

52

In C ++ ci sono iteratori, ma l'implementazione di un iteratore non è semplice: è necessario consultare i concetti dell'iteratore e progettare attentamente la nuova classe iteratore per implementarli. Per fortuna, Boost ha un modello iterator_facade che dovrebbe aiutare a implementare gli iteratori e i generatori compatibili con l'iteratore.

A volte una coroutine stackless può essere utilizzata per implementare un iteratore .

PS Vedi anche questo articolo che menziona sia un switchhack di Christopher M. Kohlhoff che Boost.Coroutine di Oliver Kowalke. Il lavoro di Oliver Kowalke è un follow- on Boost.Coroutine da Giovanni P. Deretta.

PS Penso che tu possa anche scrivere una sorta di generatore con lambda :

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;

O con un funtore:

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 Ecco un generatore implementato con le coroutine 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

Poiché Boost.Coroutine2 ora lo supporta molto bene (l'ho trovato perché volevo risolvere esattamente lo stesso yieldproblema), sto postando il codice C ++ che corrisponde alla tua intenzione originale:

#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 questo esempio, pair_sequencenon accetta argomenti aggiuntivi. Se necessario, std::bindo deve essere utilizzato un lambda per generare un oggetto funzione che accetta solo un argomento (di push_type), quando viene passato al coro_t::pull_typecostruttore.


Nota che Coroutine2 richiede c ++ 11, per il quale visual studio 2013 non è sufficiente in quanto è supportato solo parzialmente.
Weston

5

Tutte le risposte che implicano la scrittura del proprio iteratore sono completamente sbagliate. Tali risposte perdono completamente il punto dei generatori Python (una delle caratteristiche più grandi e uniche del linguaggio). La cosa più importante dei generatori è che l'esecuzione riprende da dove era stata interrotta. Questo non accade agli iteratori. Invece, è necessario memorizzare manualmente le informazioni sullo stato in modo tale che quando l'operatore ++ o l'operatore * viene chiamato di nuovo, le informazioni corrette sono presenti all'inizio della chiamata di funzione successiva. Questo è il motivo per cui scrivere il proprio iteratore C ++ è un enorme dolore; mentre i generatori sono eleganti e facili da leggere e scrivere.

Non penso che ci sia un buon analogo per i generatori Python in C ++ nativo, almeno non ancora (si dice che la resa atterrerà in C ++ 17 ). Puoi ottenere qualcosa di simile ricorrendo a terze parti (ad es. Il suggerimento Boost di Yongwei) o eseguendo il tuo.

Direi che la cosa più vicina nel C ++ nativo sono i thread. Un thread può mantenere un insieme sospeso di variabili locali e può continuare l'esecuzione da dove era stato interrotto, in modo molto simile ai generatori, ma è necessario eseguire il rollio di un po 'di infrastruttura aggiuntiva per supportare la comunicazione tra l'oggetto generatore e il suo chiamante. Per esempio

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

Questa soluzione ha però diversi svantaggi:

  1. I thread sono "costosi". La maggior parte delle persone considererebbe questo un uso "stravagante" dei thread, specialmente quando il tuo generatore è così semplice.
  2. Ci sono un paio di azioni di pulizia che devi ricordare. Questi potrebbero essere automatizzati, ma avresti bisogno di ancora più infrastrutture, che ancora una volta potrebbero essere viste come "troppo stravaganti". Ad ogni modo, le pulizie di cui hai bisogno sono:
    1. out-> close ()
    2. generator.join ()
  3. Questo non ti permette di fermare il generatore. Potresti apportare alcune modifiche per aggiungere quella capacità, ma aggiunge confusione al codice. Non sarebbe mai stato così pulito come la dichiarazione di rendimento di Python.
  4. Oltre a 2, ci sono altri bit di boilerplate necessari ogni volta che vuoi "istanziare" un oggetto generatore:
    1. Parametro Channel * out
    2. Variabili aggiuntive in main: coppie, generatore

Stai confondendo la sintassi con la funzionalità. Alcune risposte sopra in realtà consentono al C ++ di riprendere l'esecuzione da dove è stata interrotta durante l'ultima chiamata. Non è niente di magico. È un dato di fatto, Python è implementato in C, quindi tutto ciò che è possibile in Python è possibile in C, anche se non altrettanto conveniente.
Edy

@edy Non è già affrontato nel primo paragrafo? Non sta affermando che funzionalità equivalenti non possono essere create nel C ++ convenzionale, ma solo che è "un dolore gigantesco".
Kaitain

@Kaitain La domanda qui non è se sia un problema fare un generatore in C ++, ma se esiste un modello per farlo. Le sue affermazioni che l'approccio "manca il punto", che la "cosa più vicina" sono i fili ... sono solo fuorvianti. È un dolore? Uno potrebbe leggere le altre risposte e decidere da solo.
Edy

@edy Ma questo non finisce per essere un punto vacuo, dato che tutti i linguaggi completi di Turing sono in definitiva capaci della stessa funzionalità? "Tutto ciò che è possibile in X è possibile in Y" è garantito per tutti questi linguaggi, ma questa non mi sembra essere un'osservazione molto illuminante.
Kaitain

@Kaitain Proprio perché tutte le lingue complete di Turing dovrebbero avere le stesse capacità, quindi la domanda su come implementare una funzionalità in un'altra lingua è legittima. Niente di ciò che ha Python non può essere realizzato da un altro linguaggio; la domanda è efficienza e manutenibilità. In entrambi i casi, C ++ sarebbe una buona scelta (r).
Edy


2

Se hai bisogno di farlo solo per un numero relativamente piccolo di generatori specifici, puoi implementarli come una classe, dove i dati del membro sono equivalenti alle variabili locali della funzione del generatore Python. Quindi hai una funzione successiva che restituisce la prossima cosa che il generatore produrrebbe, aggiornando lo stato interno mentre lo fa.

Questo è fondamentalmente simile a come sono implementati i generatori Python, credo. La differenza principale è che possono ricordare un offset nel bytecode per la funzione del generatore come parte dello "stato interno", il che significa che i generatori possono essere scritti come loop contenenti rendimenti. Dovresti invece calcolare il valore successivo dal precedente. Nel caso del tuopair_sequence , è piuttosto banale. Potrebbe non essere per generatori complessi.

Hai anche bisogno di un modo per indicare la risoluzione. Se quello che stai restituendo è "simile a un puntatore" e NULL non dovrebbe essere un valore cedibile valido, potresti usare un puntatore NULL come indicatore di terminazione. Altrimenti è necessario un segnale fuori banda.


1

Qualcosa di simile è molto simile:

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

Usare l'operatore () è solo una questione di cosa vuoi fare con questo generatore, potresti anche costruirlo come flusso e assicurarti che si adatti a un istream_iterator, per esempio.


1

Utilizzando 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

Qualcosa di simile a questo :

Esempio di utilizzo:

using ull = unsigned long long;

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

    return 0;
}

Stamperà i numeri da 0 a 99


0

Bene, oggi stavo anche cercando una facile implementazione della raccolta in C ++ 11. In realtà sono rimasto deluso, perché tutto ciò che ho trovato è troppo lontano da cose come i generatori Python, o l'operatore di rendimento C # ... o troppo complicato.

Lo scopo è quello di fare una raccolta che emetterà i suoi oggetti solo quando sarà richiesta.

Volevo che fosse così:

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

Ho trovato questo post, la migliore risposta IMHO era su boost.coroutine2, di Yongwei Wu . Poiché è il più vicino a ciò che l'autore voleva.

Vale la pena imparare le Couroutines boost .. E forse lo farò nei fine settimana. Ma finora sto usando la mia implementazione molto piccola. Spero che aiuti qualcun altro.

Di seguito è riportato un esempio di utilizzo e quindi implementazione.

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

Questa risposta funziona in C (e quindi penso che funzioni anche 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;
}

Questo è un modo semplice e non orientato agli oggetti per imitare un generatore. Questo ha funzionato come previsto per me.


-1

Proprio come una funzione simula il concetto di pila, i generatori simulano il concetto di coda. Il resto è semantica.

Come nota a margine, puoi sempre simulare una coda con uno stack utilizzando uno stack di operazioni invece dei dati. Ciò che in pratica significa è che puoi implementare un comportamento simile a una coda restituendo una coppia, il cui secondo valore ha la funzione successiva da chiamare o indica che non abbiamo valori. Ma questo è più generale di quello che fa rendimento vs rendimento. Permette di simulare una coda di valori qualsiasi piuttosto che valori omogenei che ci si aspetta da un generatore, ma senza mantenere una coda interna completa.

Più specificamente, poiché C ++ non ha un'astrazione naturale per una coda, è necessario utilizzare costrutti che implementano una coda internamente. Quindi la risposta che ha dato l'esempio con gli iteratori è un'implementazione decente del concetto.

Ciò significa praticamente che puoi implementare qualcosa con funzionalità di coda ridotta all'osso se vuoi solo qualcosa di veloce e quindi consumare i valori della coda proprio come utilizzeresti i valori ottenuti da un generatore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.