Modèle de générateur C ++ équivalent à Python


117

J'ai un exemple de code Python que je dois imiter en C ++. Je n'ai besoin d'aucune solution spécifique (comme des solutions de rendement basées sur la co-routine, bien qu'elles soient également des réponses acceptables), j'ai simplement besoin de reproduire la sémantique d'une manière ou d'une autre.

Python

Il s'agit d'un générateur de séquence basique, clairement trop volumineux pour stocker une version matérialisée.

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

Le but est de conserver deux instances de la séquence ci-dessus et de les parcourir en semi-lockstep, mais en morceaux. Dans l'exemple ci-dessous, first_passutilise la séquence de paires pour initialiser le tampon, et second_passrégénère la même séquence exacte et traite à nouveau le tampon.

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

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

C ++

La seule chose que je peux trouver pour une solution en C ++ est d'imiter yieldavec les coroutines C ++, mais je n'ai trouvé aucune bonne référence sur la façon de procéder. Je suis également intéressé par des solutions alternatives (non générales) pour ce problème. Je n'ai pas assez de budget mémoire pour conserver une copie de la séquence entre les passes.


Comme vous pouvez le voir ici, stackoverflow.com/questions/3864410/... coroutine n'est pas une bonne idée à implémenter. Ne pouvez-vous pas le faire avec une lecture tamponnée? stackoverflow.com/questions/4685862/…
batbaatar

Les itérateurs C ++ devraient prendre en charge quelque chose comme ça.
Lalaland

Réponses:


79

Les générateurs existent en C ++, juste sous un autre nom: Input Iterators . Par exemple, lire à partir de std::cinest similaire à avoir un générateur de char.

Vous devez simplement comprendre ce que fait un générateur:

  • il y a un tas de données: les variables locales définissent un état
  • il existe une méthode init
  • il existe une méthode "suivante"
  • il existe un moyen de signaler la fin

Dans votre exemple trivial, c'est assez simple. Conceptuellement:

struct State { unsigned i, j; };

State make();

void next(State&);

bool isDone(State const&);

Bien sûr, nous emballons ceci comme une classe appropriée:

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

Alors hum ouais ... peut-être que C ++ est un peu plus bavard :)


2
J'ai accepté votre réponse (merci!) Car elle est techniquement correcte pour la question que j'ai posée. Avez-vous des pointeurs pour les techniques dans les cas où la séquence à générer est plus complexe, ou est-ce que je bat juste un cheval mort ici avec C ++ et que les coroutines sont vraiment le seul moyen de généralisation?
Noah Watkins

3
@NoahWatkins: les coroutines permettent une implémentation facile lorsque les langues les prennent en charge. Malheureusement, C ++ ne le fait pas, donc l'itération est plus facile. Si vous avez vraiment besoin de coroutines, vous avez en fait besoin d'un thread complet pour contenir la "pile" de votre appel de fonction sur le côté. Il est certainement exagéré d'ouvrir une telle boîte de vers juste pour cela dans cet exemple, mais votre kilométrage peut varier en fonction de vos besoins réels.
Matthieu M.

1
Une implémentation de coroutine non basée sur les threads est disponible dans Boost boost.org/doc/libs/1_57_0/libs/coroutine/doc/html/index.html avec une proposition de standardisation ici: open-std.org/jtc1/sc22/ wg21 / docs / papers / 2014 / n3985.pdf
boycy

2
@boycy: Il existe en fait plusieurs propositions de coroutines, notamment une sans pile et l'autre pleine. C'est dur à craquer, alors pour l'instant j'attends. En attendant, les coroutines sans pile sont implémentables presque directement en tant qu'itérateurs d'entrée (juste, sans le sucre).
Matthieu M.

3
Pourtant, similaires, les itérateurs ne sont pas les mêmes que les générateurs.
Огњен Шобајић

52

En C ++, il y a des itérateurs, mais implémenter un itérateur n'est pas simple: il faut consulter les concepts d'itérateur et concevoir soigneusement la nouvelle classe d'itérateur pour les implémenter. Heureusement, Boost a un modèle iterator_facade qui devrait aider à implémenter les itérateurs et les générateurs compatibles avec les itérateurs.

Parfois, une coroutine sans pile peut être utilisée pour implémenter un itérateur .

PS Voir aussi cet article qui évoque à la fois un switchhack de Christopher M. Kohlhoff et Boost.Coroutine d'Oliver Kowalke. Le travail d'Oliver Kowalke fait suite à Boost , Coroutine de Giovanni P. Deretta.

PS je pense que vous pouvez aussi écrire une sorte de générateur avec des 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;

Ou avec un foncteur:

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 Voici un générateur implémenté avec les coroutines 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

Puisque Boost.Coroutine2 le supporte maintenant très bien (je l'ai trouvé parce que je voulais résoudre exactement le même yieldproblème), je poste le code C ++ qui correspond à votre intention initiale:

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

Dans cet exemple, pair_sequencene prend pas d'arguments supplémentaires. S'il le faut, std::bindou un lambda doit être utilisé pour générer un objet fonction qui ne prend qu'un seul argument (de push_type), lorsqu'il est passé au coro_t::pull_typeconstructeur.


Notez que Coroutine2 nécessite C ++ 11, pour lequel Visual Studio 2013 est insuffisant car il n'est que partiellement pris en charge.
Weston

5

Toutes les réponses qui impliquent l'écriture de votre propre itérateur sont complètement fausses. De telles réponses passent totalement à côté de l'intérêt des générateurs Python (l'une des fonctionnalités les plus importantes et uniques du langage). La chose la plus importante à propos des générateurs est que l'exécution reprend là où elle s'était arrêtée. Cela n'arrive pas aux itérateurs. Au lieu de cela, vous devez stocker manuellement les informations d'état de sorte que lorsque l'opérateur ++ ou l'opérateur * est appelé à nouveau, les bonnes informations sont en place au tout début de l'appel de fonction suivant. C'est pourquoi écrire votre propre itérateur C ++ est une tâche gigantesque; tandis que les générateurs sont élégants et faciles à lire et à écrire.

Je ne pense pas qu'il existe un bon analogue pour les générateurs Python en C ++ natif, du moins pas encore (il y a un rummor qui aboutira à C ++ 17 ). Vous pouvez obtenir quelque chose de similaire en recourant à un tiers (par exemple la suggestion Boost de Yongwei) ou en utilisant le vôtre.

Je dirais que ce qui se rapproche le plus en C ++ natif, ce sont les threads. Un thread peut maintenir un ensemble suspendu de variables locales et continuer l'exécution là où il s'est arrêté, tout comme les générateurs, mais vous devez déployer un peu d'infrastructure supplémentaire pour prendre en charge la communication entre l'objet générateur et son appelant. Par exemple

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

Cette solution présente cependant plusieurs inconvénients:

  1. Les fils sont "chers". La plupart des gens considéreraient cela comme une utilisation "extravagante" des threads, surtout lorsque votre générateur est si simple.
  2. Il y a quelques actions de nettoyage dont vous devez vous souvenir. Celles-ci pourraient être automatisées, mais vous auriez besoin d'encore plus d'infrastructure, ce qui, là encore, est susceptible d'être considéré comme "trop ​​extravagant". Quoi qu'il en soit, les nettoyages dont vous avez besoin sont:
    1. out-> fermer ()
    2. generateur.join ()
  3. Cela ne vous permet pas d'arrêter le générateur. Vous pouvez apporter des modifications pour ajouter cette capacité, mais cela ajoute du fouillis au code. Ce ne serait jamais aussi propre que l'instruction yield de Python.
  4. En plus de 2, il y a d'autres bits de passe-partout qui sont nécessaires chaque fois que vous voulez "instancier" un objet générateur:
    1. Paramètre Channel * out
    2. Variables supplémentaires dans main: paires, générateur

Vous confondez syntaxe et fonctionnalité. Quelques réponses ci-dessus permettent en fait au C ++ de reprendre l'exécution là où elle s'était arrêtée lors du dernier appel. Ce n'est rien de magique. En fait, Python est implémenté en C, donc tout ce qui est possible en Python est possible en C, mais pas aussi pratique.
Edy

@edy N'est-ce pas déjà abordé dans le premier paragraphe? Il ne prétend pas qu'une fonctionnalité équivalente ne peut pas être créée en C ++ conventionnel, seulement que c'est «une douleur gigantesque».
Kaitain

@Kaitain La question ici n'est pas de savoir si c'est difficile de faire un générateur en C ++, mais s'il existe un modèle pour le faire. Ses affirmations selon lesquelles l'approche "manque le point", que la "chose la plus proche" est les fils ... sont tout simplement trompeuses. Est-ce une douleur? On pouvait lire les autres réponses et décider par lui-même.
Edy

@edy Mais cela ne finit-il pas par être un point vide, étant donné que tous les langages complets de Turing sont finalement capables de la même fonctionnalité? «Tout ce qui est possible en X est possible en Y» est garanti pour toutes ces langues, mais cela ne me semble pas être une observation très éclairante.
Kaitain le

@Kaitain Précisément parce que tous les langages complets de Turing devraient avoir la même capacité, donc la question de savoir comment implémenter une fonctionnalité dans une autre langue est légitime. Rien de ce que Python possède ne peut être accompli par un autre langage; la question est l'efficacité et la maintenabilité. Dans les deux cas, C ++ serait un bon choix (r).
Edy le


2

Si vous n'avez besoin de le faire que pour un nombre relativement petit de générateurs spécifiques, vous pouvez implémenter chacun en tant que classe, où les données membres sont équivalentes aux variables locales de la fonction de générateur Python. Ensuite, vous avez une fonction suivante qui renvoie la prochaine chose que le générateur produirait, mettant à jour l'état interne comme il le fait.

C'est fondamentalement similaire à la façon dont les générateurs Python sont implémentés, je crois. La principale différence étant qu'ils peuvent se souvenir d'un décalage dans le bytecode pour la fonction de générateur dans le cadre de "l'état interne", ce qui signifie que les générateurs peuvent être écrits comme des boucles contenant des rendements. Vous devrez à la place calculer la valeur suivante à partir de la précédente. Dans le cas de votrepair_sequence , c'est assez trivial. Ce n'est peut-être pas pour les générateurs complexes.

Vous avez également besoin d'un moyen d'indiquer la résiliation. Si ce que vous retournez est "semblable à un pointeur", et que NULL ne doit pas être une valeur valide, vous pouvez utiliser un pointeur NULL comme indicateur de fin. Sinon, vous avez besoin d'un signal hors bande.


1

Quelque chose comme ça est très similaire:

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

Utiliser l'opérateur () n'est qu'une question de savoir ce que vous voulez faire avec ce générateur, vous pouvez également le construire sous forme de flux et vous assurer qu'il s'adapte à un istream_iterator, par exemple.


1

Utilisation de 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

Quelque chose comme ça :

Exemple d'utilisation:

using ull = unsigned long long;

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

    return 0;
}

Imprime les nombres de 0 à 99


0

Eh bien, aujourd'hui, je cherchais également une implémentation de collection facile sous C ++ 11. En fait, j'ai été déçu, car tout ce que j'ai trouvé est trop éloigné de choses comme les générateurs python, ou l'opérateur de rendement C # ... ou trop compliqué.

Le but est de faire une collection qui n'émettra ses éléments que lorsque cela sera nécessaire.

Je voulais que ce soit comme ça:

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

J'ai trouvé ce message, la meilleure réponse à mon humble avis était sur boost.coroutine2, par Yongwei Wu . Puisqu'il est le plus proche de ce que l'auteur voulait.

Il vaut la peine d'apprendre les couroutines boost .. Et je le ferai peut-être le week-end. Mais jusqu'à présent, j'utilise ma toute petite implémentation. J'espère que cela aide quelqu'un d'autre.

Voici un exemple d'utilisation, puis de mise en œuvre.

Exemple.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

Cette réponse fonctionne en C (et je pense donc que cela fonctionne aussi en 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;
}

C'est une manière simple et non orientée objet d'imiter un générateur. Cela a fonctionné comme prévu pour moi.


-1

Tout comme une fonction simule le concept de pile, les générateurs simulent le concept de file d'attente. Le reste est de la sémantique.

En remarque, vous pouvez toujours simuler une file d'attente avec une pile en utilisant une pile d'opérations au lieu de données. En pratique, cela signifie que vous pouvez implémenter un comportement de type file d'attente en renvoyant une paire, dont la deuxième valeur a la fonction suivante à appeler ou indique que nous n'avons plus de valeurs. Mais c'est plus général que ce que fait le rendement par rapport au rendement. Il permet de simuler une file d'attente de toutes les valeurs plutôt que des valeurs homogènes que vous attendez d'un générateur, mais sans garder une file d'attente interne complète.

Plus précisément, étant donné que C ++ n'a pas d'abstraction naturelle pour une file d'attente, vous devez utiliser des constructions qui implémentent une file d'attente en interne. La réponse qui a donné l'exemple avec les itérateurs est donc une implémentation décente du concept.

Cela signifie pratiquement que vous pouvez implémenter quelque chose avec une fonctionnalité de file d'attente simple si vous voulez juste quelque chose de rapide et ensuite consommer les valeurs de la file d'attente comme vous le feriez avec les valeurs générées par un générateur.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.