Modèle de générateur c++ équivalent à Python
J'ai un exemple de code Python que je dois imiter en C++. Je n'ai besoin d'aucune solution spécifique (telle que 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
Ceci est un générateur de séquence de base, clairement trop grand 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 maintenir deux instances de la séquence ci-dessus, et de les itérer en semi-lockstep, mais dans les 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 peux trouver pour une solution en c++ est d'imiter yield
avec des coroutines C++, mais je n'ai pas trouvé de bonne référence sur la façon de le faire. 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 en conserver une copie de la séquence entre les passes.
10 réponses
Les générateurs existent en C++, juste sous un autre nom: itérateurs D'entrée . Par exemple, la lecture de std::cin
est semblable à 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 état
- Il existe une méthode d'initialisation
- Il existe une méthode" next "
- Il existe un moyen de signaler la terminaison
Dans votre exemple trivial, c'est assez facile. Conceptuellement:
struct State { unsigned i, j; };
State make();
void next(State&);
bool isDone(State const&);
Bien Sûr, nous enveloppons 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 verbeux:)
En C++, il y a des itérateurs, mais l'implémentation d'un itérateur n'est pas simple: il faut consulter les concepts iterator et concevoir soigneusement la nouvelle classe iterator 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 empilée peut être utilisée pour implémenter un itérateur .
P.S. Voir aussi Cet article qui mentionne les deux un switch
hack par Christopher M. Kohlhoff et Boost.Coroutine par Oliver Kowalke. Le travail d'Oliver Kowalke est un suivi sur Boost.Coroutine {[6] } par 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 coroutinesMordor :
#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;
}
Depuis Coup De Pouce.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 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_sequence
ne prend pas d'arguments supplémentaires. Si nécessaire, std::bind
ou un lambda doit être utilisé pour générer un objet de fonction qui ne prend qu'un seul argument (de push_type
), lorsqu'il est passé au constructeur coro_t::pull_type
.
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 cherchez. Les générateurs globaux devraient être disponibles en C++17 car il ne s'agit que d'une fonctionnalité expérimentale de Microsoft VC.
Si vous n'avez besoin de le faire que pour un nombre relativement faible 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 générateur Python. Ensuite, vous avez une fonction suivante qui renvoie la prochaine chose que le générateur donnerait, en 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 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 plutôt calculer la valeur suivante 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 renvoyez est "pointeur", et que NULL ne devrait pas être une valeur yieldable valide, Vous pouvez utiliser un pointeur NULL comme terminaison indicateur. Sinon vous avez besoin d'un signal à bande.
Toutes les réponses qui impliquent l'écriture de votre propre itérateur sont complètement fausses. De telles réponses manquent complètement le point des générateurs Python (l'une des caractéristiques les plus grandes 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. Ce n'est pas le cas des 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 {[3] } de la suivante appel de fonction. 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 un rummor qui yield atterrira en C++17). Vous pouvez obtenir quelque chose de similaire en recourant à un tiers (par exemple, la suggestion de Boost de Yongwei), ou en roulant le 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 peut continuer l'exécution là où elle s'est arrêtée, tout comme les générateurs, mais vous devez lancer 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 a cependant plusieurs inconvénients:
- les Threads sont "chers". La plupart des gens considéreraient cela comme une utilisation" extravagante " des threads, surtout lorsque votre générateur est si simple.
- voilà sont quelques actions de nettoyage que vous devez vous rappeler. Ceux-ci pourraient être automatisés, mais vous auriez besoin d'encore plus d'infrastructure, ce qui, encore une fois, est susceptible d'être considéré comme "trop extravagant". Quoi qu'il en soit, les nettoyages dont vous avez besoin sont:
- out- > close ()
- générateur.join ()
- cela ne vous permet pas d'arrêter generator. Vous pouvez apporter quelques modifications pour ajouter cette capacité, mais cela ajoute de l'encombrement au code. Il ne serait jamais aussi propre que le rendement de Python déclaration.
- 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:
- Canal * paramètre de sortie
- variables Supplémentaires dans les principales paires, générateur
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;
}
L'utilisation de l'opérateur () n'est qu'une question de ce que vous voulez faire avec ce générateur, vous pouvez également le construire en tant que flux et vous assurer qu'il s'adapte à un istream_iterator, par exemple.
Tout comme une fonction simule le concept d'une pile, les générateurs simulent le concept d'une file d'attente. Le reste est de la sémantique.
Comme note de côté, vous pouvez toujours simuler une file d'attente avec une pile en utilisant une pile d'opérations au lieu de données. Ce que cela signifie pratiquement, c'est que vous pouvez implémenter un comportement de type file d'attente en retournant une paire, dont la deuxième valeur a la fonction suivante à appeler ou indique que nous sommes hors de valeurs. Mais c'est plus général que ce rendement vs retour. 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 pleine file d'attente interne.
Plus précisément, puisque 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. Donc, la réponse qui a donné l'exemple avec des itérateurs est une implémentation décente du concept.
Ce que cela signifie pratiquement, c'est que vous pouvez implémenter quelque chose avec fonctionnalité de file d'attente bare-bones si vous voulez juste quelque chose de rapide, puis consommez les valeurs de la file d'attente comme vous consommeriez les valeurs générées par un générateur.
Utiliser plage-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;
}