web-dev-qa-db-fra.com

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

J'ai quelques exemples de code Python que je dois imiter en C++. Je n’exige pas de solution spécifique (comme des solutions de rendement basées sur une co-routine, bien que ce soient aussi des réponses acceptables), je dois simplement reproduire la sémantique d’une manière ou d’une autre.

Python

Il s'agit d'un générateur de séquence de base, 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 demi-pas, mais en morceaux. Dans l'exemple ci-dessous, le first_pass utilise la séquence de paires pour initialiser le tampon et le second_pass régénère le 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 puisse trouver pour une solution en C++ est d’imiter yield avec des 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) à ce problème. Je n'ai pas assez de budget mémoire pour conserver une copie de la séquence entre les passes.

86
Noah Watkins

Les générateurs existent en C++, juste sous un autre nom: Iterators d'entrée . Par exemple, lire à partir de std::cin revient à avoir un générateur de char.

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

  • il y a un blob de données: les variables locales définissent un state
  • il y a une méthode init
  • il y a une méthode "next"
  • il y a 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 encapsulons cela 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 :)

58
Matthieu M.

En C++, il existe des itérateurs, mais la mise en œuvre d'un itérateur n'est pas simple: il faut consulter les concepts iterator et concevoir soigneusement la nouvelle classe d'itérateurs 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 .

P.S. Voir aussi cet article qui mentionne un piratage switch de Christopher M. Kohlhoff et Boost.Coroutine d’Oliver Kowalke. Le travail d'Oliver Kowalke est un suivi du Boost.Coroutine de Giovanni P. Deretta.

P.S. Je pense que vous pouvez aussi écrire une sorte de générateur avec 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;

P.S. 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;
}
39
ArtemGr

Depuis Boost.Coroutine2 le supporte maintenant très bien (je l’ai trouvé parce que je voulais résoudre exactement le même problème yield), je publie 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_sequence ne prend pas d'arguments supplémentaires. Si nécessaire, std::bind ou un lambda devrait être utilisé pour générer un objet fonction qui ne prend qu'un seul argument (de Push_type), lorsqu’il est passé au constructeur coro_t::pull_type.

19
Yongwei Wu

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é 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'est 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é de nouveau, les informations appropriées sont en place au tout début de l'appel de fonction suivant. C'est pourquoi écrire votre propre itérateur C++ est une douleur gigantesque. alors que les générateurs sont élégants et faciles à lire et à écrire.

Je ne pense pas qu'il y ait un bon analogue pour les générateurs Python en C++ natif, du moins pas encore (il y a une rumeur selon laquelle rendement va atterrir en C++ 17 ). Vous pouvez obtenir quelque chose de similaire en faisant appel à une tierce partie (par exemple, la suggestion Boost de Yongwei), ou en lançant la vôtre.

Je dirais que la chose la plus proche en C++ natif est les threads. Un thread peut maintenir un ensemble suspendu de variables locales et poursuivre l’exécution là où il s’est arrêté, tout comme les générateurs, mais vous devez disposer d’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èrent qu'il s'agit d'une utilisation "extravagante" de threads, en particulier lorsque votre générateur est si simple.
  2. Il y a quelques actions de nettoyage dont vous devez vous rappeler. Celles-ci pourraient être automatisées, mais il vous faudrait encore plus d'infrastructure, ce qui, encore une fois, risque d'être considéré comme "trop ​​extravagant". Quoi qu'il en soit, les nettoyages dont vous avez besoin sont les suivants:
    1. out-> close ()
    2. generateur.join ()
  3. Cela ne vous permet pas d'arrêter le générateur. Vous pouvez faire quelques modifications pour ajouter cette capacité, mais cela ajoute du fouillis au code. Il ne serait jamais aussi clair que la déclaration de rendement de Python.
  4. En plus de 2, il existe 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 la variable principale: paires, générateur
3
allyourcode

Vous devriez probablement vérifier les générateurs dans std :: experimental dans Visual Studio 2015, par exemple: https://blogs.msdn.Microsoft.com/vcblog/2014/11/12/resumable-functions-in-c/

Je pense que c'est exactement ce que vous recherchez. Les générateurs globaux devraient être disponibles en C++ 17 car il ne s'agit que de la fonctionnalité expérimentale Microsoft VC.

Si vous ne devez effectuer cette opération que pour un nombre relativement petit de générateurs spécifiques, vous pouvez les implémenter en tant que classe, où les données de membre 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 produira, mettant à jour l'état interne comme il le fait.

Ceci 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 en tant que partie intégrante de "l'état interne", ce qui signifie que les générateurs peuvent être écrits sous forme de boucles contenant des rendements. Vous devriez plutôt calculer la valeur suivante à partir de la précédente. Dans le cas de votre pair_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 ressemble à un "pointeur" et que la valeur NULL ne doit pas être valide, vous pouvez utiliser un pointeur NULL comme indicateur de fin. Sinon, vous avez besoin d'un signal hors bande.

2
Ben

Quelque chose comme ceci 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;
}

L'utilisation de operator () dépend uniquement de 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
lip

Utiliser 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

Quelque chose comme ceci :

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

Imprimera les chiffres de 0 à 99

0
smac89

Eh bien, aujourd’hui, je recherchais également une implémentation facile de la collecte 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 de python, ou l’opérateur de rendement C # ... ou trop compliqué.

Le but est de faire une collection qui n'émettra ses articles 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 post, IMHO meilleure réponse était sur boost.coroutine2, par Yongwei Wu . Puisque c'est le plus proche de ce que l'auteur voulait.

Cela vaut la peine d’apprendre des cours de stimulation… et je le ferai peut-être les fins de semaine. Mais jusqu'à présent, j'utilise ma très petite implémentation. J'espère que ça aide quelqu'un d'autre.

Vous trouverez ci-dessous un exemple d'utilisation, puis une implémentation.

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