Эквивалентный образец генератора C ++ для Python - PullRequest
97 голосов
/ 30 января 2012

У меня есть пример кода Python, который мне нужно имитировать в C ++.Мне не требуется никакого конкретного решения (например, решения для доходности, основанного на совместной подпрограмме, хотя они также были бы приемлемыми ответами), мне просто нужно каким-то образом воспроизвести семантику.

Python

Это базовый генератор последовательности, явно слишком большой, чтобы хранить материализованную версию.

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

Цель состоит в том, чтобы поддерживать два экземпляра последовательности, описанной выше, и выполнять их итерацию в полусвободном шаге, но порциями,В приведенном ниже примере first_pass использует последовательность пар для инициализации буфера, а second_pass регенерирует такую ​​же точную последовательность и снова обрабатывает буфер.

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

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

C ++

Единственное, что я могу найти для решения в C ++, это имитировать yield с сопрограммами C ++, но я не нашел какой-либо хорошей ссылки на то, как это сделать.Я также заинтересован в альтернативных (не общих) решениях этой проблемы.У меня недостаточно памяти, чтобы хранить копию последовательности между проходами.

Ответы [ 11 ]

64 голосов
/ 30 января 2012

Генераторы существуют в C ++, просто под другим именем: Итераторы ввода .Например, чтение из std::cin похоже на наличие генератора char.

. Вам просто нужно понять, что делает генератор:

  • существует блок данных: локальные переменные определяют состояние
  • есть метод init
  • есть метод "next"
  • есть способ сообщить о завершении

В вашем тривиальном примере это достаточно просто.Концептуально:

struct State { unsigned i, j; };

State make();

void next(State&);

bool isDone(State const&);

Конечно, мы обертываем это как правильный класс:

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

Так что, да ... может быть, C ++ немного более многословен:)

42 голосов
/ 05 октября 2012

В C ++ есть итераторы, но реализовать итератор не так просто: нужно обратиться к понятиям итератора и тщательно спроектировать новый класс итератора для их реализации. К счастью, Boost имеет шаблон iterator_facade , который должен помочь в реализации итераторов и генераторов, совместимых с итераторами.

Иногда сопрограмма без стека может использоваться для реализации итератора .

P.S. См. Также эту статью , в которой упоминается как хакер switch Кристофера М. Колхоффа, так и Boost.Coroutine Оливер Ковальке. Работа Оливера Ковальке является продолжением на Boost.Coroutine Джованни П. Деретты.

P.S. Я думаю, что вы также можете написать вид генератора с лямбдами :

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;

Или с функтором:

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. Вот генератор, реализованный с сопрограммами 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;
}
19 голосов
/ 06 августа 2016

Поскольку Boost.Coroutine2 теперь очень хорошо это поддерживает (я нашел это потому, что хотел решить точно такую ​​же проблему yield), я публикую код C ++, который соответствует вашему первоначальному замыслу: *

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

В этом примере pair_sequence не принимает дополнительных аргументов. Если это необходимо, std::bind или лямбда-выражение следует использовать для создания объекта функции, который принимает только один аргумент (из push_type), когда он передается конструктору coro_t::pull_type.

4 голосов
/ 01 января 2017

Все ответы, которые включают в себя написание собственного итератора, совершенно неверны.Такие ответы полностью упускают из виду генераторы Python (одна из величайших и уникальных особенностей языка).Самая важная вещь в генераторах - это то, что выполнение начинается там, где оно остановилось.Это не происходит с итераторами.Вместо этого вы должны вручную сохранить информацию о состоянии, чтобы при повторном вызове оператора ++ или оператора * правильная информация находилась в в самом начале следующего вызова функции.Вот почему написание собственного итератора C ++ - гигантская боль;в то время как генераторы элегантны и легко читаются + пишутся.

Я не думаю, что есть хороший аналог для генераторов Python в нативном C ++, по крайней мере, пока (есть слух, что даетприземлится в C ++ 17 ).Вы можете получить что-то похожее, прибегнув к сторонним разработчикам (например, предложению Yongwei's Boost) или выбрав собственное.

Я бы сказал, что в нативном C ++ самая близкая вещь - это потоки.Поток может поддерживать приостановленный набор локальных переменных и может продолжать выполнение там, где он остановился, очень похоже на генераторы, но вам нужно развернуть немного дополнительной инфраструктуры для поддержки связи между объектом генератора и его вызывающей стороной.Например,

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

У этого решения есть несколько недостатков:

  1. Потоки "дороги".Большинство людей считают это «экстравагантным» использованием потоков, особенно когда ваш генератор очень прост.
  2. Есть пара действий по очистке, которые вам нужно запомнить.Они могут быть автоматизированы, но вам потребуется еще больше инфраструктуры, которая, скорее всего, будет рассматриваться как «слишком экстравагантная».В любом случае, вам нужны следующие исправления:
    1. out-> close ()
    2. generator.join ()
  3. Это не позволяет вамостановить генератор.Вы можете внести некоторые изменения, чтобы добавить эту возможность, но это добавляет беспорядок в код.Он никогда не будет таким же чистым, как оператор Python yield.
  4. В дополнение к 2, есть другие биты шаблонов, которые нужны каждый раз, когда вы хотите «создать экземпляр» объекта генератора:
    1. Channel* выходной параметр
    2. Дополнительные переменные в основном: пары, генератор
3 голосов
/ 08 февраля 2016

Возможно, вам следует проверить генераторы в std :: экспериментальный в Visual Studio 2015, например: https://blogs.msdn.microsoft.com/vcblog/2014/11/12/resumable-functions-in-c/

Я думаю, это именно то, что вы ищете. Генераторы должны быть доступны в C ++ 17, поскольку это только экспериментальная функция Microsoft VC.

2 голосов
/ 30 января 2012

Если вам нужно сделать это только для относительно небольшого числа конкретных генераторов, вы можете реализовать каждый как класс, где данные-члены эквивалентны локальным переменным функции генератора Python. Затем у вас есть следующая функция, которая возвращает следующее, что выдаст генератор, обновляя внутреннее состояние, как это происходит.

Я думаю, это в основном похоже на то, как реализованы генераторы Python. Основное отличие состоит в том, что они могут запомнить смещение в байт-код для функции генератора как часть «внутреннего состояния», что означает, что генераторы могут быть записаны как циклы, содержащие выходы. Вместо этого вам придется рассчитать следующее значение из предыдущего. В случае вашего pair_sequence это довольно тривиально. Это может быть не для сложных генераторов.

Вам также нужен какой-то способ обозначения завершения. Если то, что вы возвращаете, «похоже на указатель», а NULL не должно быть допустимым выходным значением, вы можете использовать указатель NULL в качестве индикатора завершения. В противном случае вам нужен внеполосный сигнал.

1 голос
/ 26 июля 2018

Использование 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 голос
/ 03 июля 2013

Примерно так очень похоже:

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

Использование operator () - это только вопрос того, что вы хотите сделать с этим генератором, вы также можете построить его как поток и убедиться, что оннапример, адаптируется к istream_iterator.

0 голосов
/ 09 ноября 2018

Что ж, сегодня я также искал простую реализацию коллекции под C ++ 11.На самом деле я был разочарован, потому что все, что я нашел, слишком далеко от таких вещей, как генераторы Python, оператор C # yield ... или слишком сложно.

Цель состоит в том, чтобы создать коллекцию, которая будет генерировать свои элементы только тогда, когда онатребуется.

Я хотел, чтобы это было так:

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

Я нашел этот пост, ИМХО, лучший ответ был о boost.coroutine2, Yongwei Wu .Так как это самое близкое к тому, что хотел автор.

Стоит учиться повышенным процедурам. А я, возможно, сделаю это по выходным.Но пока я использую свою очень маленькую реализацию.Надеюсь, что это поможет кому-то еще.

Ниже приведен пример использования, а затем реализация.

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 голосов
/ 09 мая 2017

Что-то вроде this :

Пример использования:

using ull = unsigned long long;

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

    return 0;
}

Напечатает числа от 0 до 99

...