Python oluşturucu modeline eşdeğer C ++


117

C ++ 'da taklit etmem gereken bazı örnek Python kodum var. Herhangi bir özel çözüme ihtiyacım yok (ortak rutin bazlı verim çözümleri gibi, kabul edilebilir cevaplar olsa da), sadece anlamsallığı bir şekilde yeniden üretmem gerekiyor.

piton

Bu, temel bir dizi üretecidir ve gerçekleştirilmiş bir sürümü saklamak için açıkça çok büyüktür.

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

Amaç, yukarıdaki dizinin iki örneğini korumak ve bunların üzerinde yarı kilit adımlarla, ancak yığınlar halinde yinelemektir. Aşağıdaki örnekte first_pass, tamponu başlatmak için çiftler dizisini kullanır second_passve aynı tam diziyi yeniden oluşturur ve tamponu tekrar işler.

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

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

C ++

C ++ 'da bir çözüm için bulabildiğim tek şey yieldC ++ coroutines ile taklit etmektir, ancak bunun nasıl yapılacağına dair iyi bir referans bulamadım. Bu problem için alternatif (genel olmayan) çözümlerle de ilgileniyorum. Geçişler arasında dizinin bir kopyasını saklamak için yeterli bellek bütçem yok.


Buradan da görebileceğiniz gibi stackoverflow.com/questions/3864410/… coroutine uygulamak iyi bir fikir değildir. Tamponlu okuma ile yapamaz mısın? stackoverflow.com/questions/4685862/…
batbaatar

C ++ yineleyiciler böyle bir şeyi desteklemelidir.
Lalaland

Yanıtlar:


79

Üreteçler, C ++ 'da başka bir ad altında bulunur: Giriş Yineleyicileri . Örneğin, kaynağından okumak, std::cinoluşturucuya sahip olmaya benzer char.

Bir jeneratörün ne yaptığını anlamanız yeterlidir:

  • bir veri bloğu var: yerel değişkenler bir durumu tanımlar
  • bir başlangıç ​​yöntemi var
  • "sonraki" bir yöntem var
  • sonlandırma sinyali vermenin bir yolu var

Sizin önemsiz örneğinizde, yeterince kolay. kavramsal olarak:

struct State { unsigned i, j; };

State make();

void next(State&);

bool isDone(State const&);

Elbette bunu uygun bir sınıf olarak ele alıyoruz:

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

Yani evet ... C ++ biraz daha ayrıntılı olabilir :)


2
Cevabınızı (teşekkürler!) Verdiğim soru için teknik olarak doğru olduğu için kabul ettim. Oluşturulması gereken sıranın daha karmaşık olduğu durumlarda teknikler için herhangi bir işaretiniz var mı, yoksa burada sadece C ++ ile ölü bir atı mı dövüyorum ve gerçekten de koroutinler genellik için tek yol mu?
Noah Watkins

3
@NoahWatkins: Coroutines, diller onları desteklediğinde kolay bir uygulama sağlar. Ne yazık ki C ++ bunu yapmaz, bu nedenle yineleme daha kolaydır. Gerçekten eşgüdümlere ihtiyacınız varsa, işlev çağrınızın "yığınını" yanda tutmak için tam gelişmiş bir iş parçacığına ihtiyacınız vardır. Bu örnekte sadece bunun için böyle bir solucan konservesi açmak kesinlikle abartılıdır , ancak kilometreniz gerçek ihtiyaçlarınıza bağlı olarak değişebilir.
Matthieu M.

1
Boost boost.org/doc/libs/1_57_0/libs/coroutine/doc/html/index.html adresinde iş parçacığı tabanlı olmayan bir coroutine uygulaması mevcuttur ve burada standartlaştırma önerisi şu adrestedir
boycy

2
@boycy: Aslında eş diziler için birden fazla öneri var, özellikle biri yığınsız ve diğer yığın dolu. Kırması zor bir ceviz, bu yüzden şimdilik bekliyorum. Bu arada, yığınsız koroutinler, doğrudan Giriş Yineleyicileri olarak (sadece şeker olmadan) uygulanabilir.
Matthieu M.

3
Yine de benzer, yineleyiciler üreteçlerle aynı değildir.
Огњен Шобајић

52

C ++ 'da yineleyiciler vardır, ancak bir yineleyici uygulamak basit değildir: yineleyici kavramlarına başvurmak ve bunları uygulamak için yeni yineleyici sınıfını dikkatlice tasarlamak gerekir. Neyse ki, Boost, yineleyicileri ve yineleyici uyumlu oluşturucuları uygulamaya yardımcı olacak bir yineleyici_facade şablonuna sahiptir.

Bazen bir yineleyici uygulamak için yığınsız bir koroutin kullanılabilir .

Not: Hem Christopher M. Kohlhoff'un bir hack'inden hem de Oliver Kowalke'nin Boost.Coroutine'den bahseden bu makaleye de bakın . Oliver Kowalke çalışması bir takip olduğu üzerinde Boost.Coroutine Giovanni P. Deretta tarafından.switch

Not : Lambdas ile bir çeşit jeneratör de yazabileceğinizi düşünüyorum :

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;

Veya bir functor ile:

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;

Not: İşte Mordor eşgüdümleriyle uygulanan bir oluşturucu:

#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

Yana Boost.Coroutine2 şimdi çok iyi destekler (Ben tam olarak aynı çözmek istediği için ben buldum yieldorijinal niyeti maçları, ben C gönderme kulüpler ++ kod sorunu):

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

Bu örnekte, pair_sequenceek argümanlar almaz. Gerekirse std::bindveya bir lambda , yapıcıya push_typeiletildiğinde yalnızca bir argüman (of ) alan bir işlev nesnesi oluşturmak için kullanılmalıdır coro_t::pull_type.


Coroutine2'nin c ++ 11'i gerektirdiğini unutmayın; bunun için visual studio 2013, yalnızca kısmen desteklendiği için yetersizdir.
Weston

5

Kendi yineleyicinizi yazmayı içeren tüm yanıtlar tamamen yanlıştır. Bu tür cevaplar, Python üreticilerinin (dilin en büyük ve benzersiz özelliklerinden biri) amacını tamamen gözden kaçırıyor. Jeneratörlerle ilgili en önemli şey, uygulamanın kaldığı yerden devam etmesidir. Bu yineleyicilere olmaz. Bunun yerine, durum bilgilerini manuel olarak depolamalısınız, öyle ki operatör ++ veya operatör * yeniden çağrıldığında, doğru bilgi sonraki işlev çağrısının en başında yer alır. Bu nedenle kendi C ++ yineleyicinizi yazmak büyük bir acıdır; oysa jeneratörler zariftir ve okuması + yazması kolaydır.

Yerel C ++ 'da Python jeneratörleri için iyi bir analog olduğunu sanmıyorum, en azından henüz değil ( C ++ 17'de verimin geleceği yönünde bir söylenti var ). Üçüncü tarafa başvurarak (örneğin Yongwei'nin Boost önerisi) veya kendi teklifinizi atarak benzer bir şey elde edebilirsiniz.

Yerel C ++ 'daki en yakın şeyin iş parçacıkları olduğunu söyleyebilirim. Bir iş parçacığı, askıya alınmış bir yerel değişkenler kümesini koruyabilir ve üretime çok benzer şekilde kaldığı yerden devam edebilir, ancak jeneratör nesnesi ile çağıran arasındaki iletişimi desteklemek için biraz ek altyapı oluşturmanız gerekir. Örneğin

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

Bu çözümün birkaç dezavantajı var:

  1. İpler "pahalıdır". Çoğu insan, özellikle de jeneratörünüz çok basit olduğunda, bunu "abartılı" bir iplik kullanımı olarak görür.
  2. Hatırlamanız gereken birkaç temizleme işlemi vardır. Bunlar otomatik hale getirilebilir, ancak daha da fazla altyapıya ihtiyacınız olacaktır ve bu da muhtemelen "aşırı abartılı" olarak görülebilir. Her neyse, ihtiyacınız olan temizlikler:
    1. sokak> close ()
    2. generator.join ()
  3. Bu, jeneratörü durdurmanıza izin vermez. Bu yeteneği eklemek için bazı değişiklikler yapabilirsiniz, ancak koda dağınıklık ekler. Python'un verim beyanı kadar temiz olmayacaktı.
  4. 2'ye ek olarak, bir jeneratör nesnesini her "somutlaştırmak" istediğinizde ihtiyaç duyulan diğer ortak metin bitleri vardır:
    1. Kanal * çıkış parametresi
    2. Ana ek değişkenler: çiftler, oluşturucu

Sözdizimi ile işlevselliği karıştırıyorsunuz. Yukarıdaki birkaç cevap aslında C ++ 'nın son çağrı sırasında kaldığı yerden yürütmeyi almasına izin veriyor. Büyülü bir şey değil. Nitekim olarak, Python edilir böylece Python mümkündür neyse de olsa değil elverişli olarak, C mümkündür, C uyguladı.
Edy

@edy Bu daha ilk paragrafta belirtilmemiş mi? Geleneksel C ++ 'da eşdeğer işlevselliğin yaratılamayacağını, sadece bunun "devasa bir acı" olduğunu iddia ediyor.
Kaitain

@Kaitain Buradaki soru, C ++ 'da jeneratör yapmak için bir sorun olup olmadığı değil, bunu yapacak bir model olup olmadığı. Yaklaşımın "noktayı kaçırdığını", "en yakın şeyin" konu olduğunu iddia ediyor ... sadece yanıltıcıdır. Acı mı? Kişi diğer cevapları okuyabilir ve kendisi karar verebilir.
Edy

@edy Ancak, tüm Turing-complete dillerinin nihayetinde aynı işlevselliği sağlayabileceği düşünüldüğünde, bu anlamsız bir nokta olmaktan çıkmaz mı? "X'de mümkün olan her şey Y'de mümkündür" gibi tüm bu diller için doğru olduğu garanti edilir, ancak bu bana pek aydınlatıcı bir gözlem gibi görünmüyor.
Kaitain

@Kaitain Tam olarak, tüm Turing-complete dillerinin sözde aynı kapasiteye sahip olması gerektiğinden, bir özelliğin başka bir dilde nasıl uygulanacağı sorusu meşrudur. Python'un sahip olduğu hiçbir şey başka bir dil tarafından gerçekleştirilemez; soru verimlilik ve sürdürülebilirliktir. Her iki açıdan da, C ++ iyi bir (r) seçim olacaktır.
Edy


2

Bunu yalnızca görece az sayıda belirli üretici için yapmanız gerekiyorsa, her birini üye verilerinin Python oluşturucu işlevinin yerel değişkenlerine eşdeğer olduğu bir sınıf olarak uygulayabilirsiniz. Ardından, jeneratörün vereceği bir sonraki şeyi döndüren, dahili durumu güncelleyen bir sonraki işleviniz olur.

Bu temelde Python jeneratörlerinin nasıl uygulandığına benziyor, inanıyorum. En büyük fark, "dahili durumun" bir parçası olarak jeneratör işlevi için bayt kodunda bir ofseti hatırlayabilmeleridir; bu, jeneratörlerin verim içeren döngüler olarak yazılabileceği anlamına gelir. Bunun yerine, bir öncekinden bir sonraki değeri hesaplamanız gerekir. Senin durumunda pair_sequence, bu oldukça önemsiz. Karmaşık jeneratörler için olmayabilir.

Ayrıca fesih belirtmek için de bir yol bulmanız gerekir. Döndürdüğünüz şey "işaretçi benzeri" ise ve NULL geçerli bir üretilebilir değer olmamalıysa, sonlandırma göstergesi olarak bir NULL gösterici kullanabilirsiniz. Aksi takdirde bant dışı sinyale ihtiyacınız vardır.


1

Bunun gibi bir şey çok benzer:

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

Operatörün () kullanılması sadece bu oluşturucu ile ne yapmak istediğinize dair bir sorudur, bunu bir akım olarak da oluşturabilir ve örneğin bir istream_iterator'a uyarlanmasını sağlayabilirsiniz.


1

Range-v3 kullanarak :

#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

Gibi bir şey bu :

Örnek kullanım:

using ull = unsigned long long;

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

    return 0;
}

0'dan 99'a kadar olan sayıları yazdıracak


0

Bugün ayrıca C ++ 11 altında kolay koleksiyon uygulaması arıyordum. Aslında hayal kırıklığına uğradım, çünkü bulduğum her şey python jeneratörleri veya C # verim operatörü gibi şeylerden çok uzak ... veya çok karmaşık.

Amaç, eşyalarını yalnızca ihtiyaç duyulduğunda yayacak koleksiyon yapmaktır.

Bunun böyle olmasını istedim:

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

Bu yazıyı buldum, IMHO'nun en iyi cevabı Yongwei Wu'nun boost.coroutine2 ile ilgiliydi . Yazarın istediğine en yakın olduğu için.

Güçlendirme kursları öğrenmeye değer .. Ve belki hafta sonları yaparım. Ama şimdiye kadar çok küçük uygulamamı kullanıyorum. Umarım başkasına yardımcı olur.

Aşağıda kullanım ve ardından uygulama örneği verilmiştir.

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

Bu cevap C'de çalışıyor (ve dolayısıyla c ++ 'da da çalıştığını düşünüyorum)

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

Bu, bir jeneratörü taklit etmenin basit, nesne yönelimli olmayan bir yoludur. Bu benim için beklendiği gibi çalıştı.


-1

Bir işlevin yığın kavramını simüle etmesi gibi, üreteçler de kuyruk kavramını simüle eder. Gerisi anlambilimdir.

Bir yan not olarak, veri yerine bir işlem yığını kullanarak bir kuyruğu her zaman bir yığınla simüle edebilirsiniz. Bunun pratik olarak anlamı, ikinci değeri çağrılacak bir sonraki işleve sahip olan veya değerlerimizin tükendiğini gösteren bir çift döndürerek kuyruk benzeri bir davranış uygulayabileceğinizdir. Ancak bu, getiriye karşı getirinin yaptığından daha geneldir. Bir jeneratörden beklediğiniz homojen değerler yerine herhangi bir değer kuyruğunu simüle etmeye izin verir, ancak tam bir dahili kuyruk tutmadan.

Daha spesifik olarak, C ++ bir kuyruk için doğal bir soyutlamaya sahip olmadığından, dahili olarak bir kuyruğu uygulayan yapıları kullanmanız gerekir. Dolayısıyla, yineleyicilerle örneği veren cevap, kavramın iyi bir uygulamasıdır.

Bunun pratik olarak anlamı, sadece hızlı bir şey istiyorsanız çıplak kemik kuyruğu işlevselliğine sahip bir şey uygulayabileceğiniz ve ardından bir jeneratörden elde edilen değerleri tükettiğiniz gibi kuyruğun değerlerini tüketebileceğinizdir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.