Эквивалент C++ для шаблона генератора Python


у меня есть пример кода 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++, но я не нашел хорошей ссылки на то, как это сделать. Меня также интересуют альтернативные (не общие) решения для этого проблема. У меня бюджета не хватит памяти, чтобы сохранить копию последовательности между проходами.

10 77

10 ответов:

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

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

  • есть большой двоичный объект данных: локальные переменные определяют a state
  • есть метод init
  • есть "следующий" способ
  • есть способ подать сигнал прекращение

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

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++ немного более подробный :)

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

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

П. С. см. также в этой статье где упоминается как switch hack by Christopher M. Kohlhoff and импульс.Корутин Оливер Kowalke. Работа Оливера Ковальке в последующем on импульс.Корутин Джованни П. Деретта.

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. Вот генератор реализован с Мордор сопрограммы:

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

С импульс.Сорутине2 теперь поддерживает его очень хорошо (я нашел его, потому что я хотел решить точно так же 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 или лямбда должна использоваться для создания объекта функции, который принимает только один аргумент (of push_type), когда он передается coro_t::pull_type конструктор.

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

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

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

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

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

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

Я не думаю, что есть хороший аналог для генераторов Python в родном C++, по крайней мере пока (есть слух, что урожай будет приземляться в C++17). Вы можете получить что-то похожее, прибегнув к стороннему (например, предложение Yongwei 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. генератор.присоединяйтесь()
  3. это не позволяет остановить генератор. Вы можете внести некоторые изменения, чтобы добавить эту способность, но это добавляет беспорядок код. Это никогда не было бы так чисто, как заявление yield Python.
  4. в дополнение к 2, есть другие биты шаблона, которые необходимы каждый раз, когда вы хотите "создать экземпляр" объекта генератора:
    1. Channel * out параметр
    2. дополнительные переменные в основном: пары, генератор

что-то вроде этого очень похоже:

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

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

Так же, как функция имитирует понятие стека, генераторы имитируют понятие очереди. Остальное-семантика.

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

более конкретно, поскольку C++ не имеет естественной абстракции для очереди, вам нужно использовать конструкции, которые реализуют очередь внутри. Поэтому ответ, который дал пример с итераторами, - это достойная реализация концепции.

Что это практически означает, что вы можете реализовать что-то с голыми костями функциональность очереди, если вы просто хотите что-то быстро, а затем потреблять значения очереди так же, как вы бы потребляли значения, полученные от генератора.

что-то вроде этой:

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

using ull = unsigned long long;

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

    return 0;
}

выведет числа от 0 до 99

С помощью диапазон-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;
}