Еквівалентний шаблон C ++ до генератора Python


117

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

Пітон

Це базовий генератор послідовностей, явно занадто великий, щоб зберігати матеріалізовану версію.

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


Як видно звідси, stackoverflow.com/questions/3864410/… coroutine не дуже вдала реалізація. Хіба ти не можеш це зробити з буферним читанням? stackoverflow.com/questions/4685862 / ...
batbaatar

C ++ ітератори повинні підтримувати щось подібне.
Лалаланд

Відповіді:


79

Генератори існують в C ++, просто під іншою назвою: Вхідні ітератори . Наприклад, читання з std::cinаналогічно наявності генератора char.

Вам просто потрібно зрозуміти, що робить генератор:

  • є крапка даних: локальні змінні визначають стан
  • існує метод 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 ++ - це більш багатослівний :)


2
Я прийняв вашу відповідь (спасибі!), Оскільки це технічно правильне питання, яке я дав. Чи є у вас покажчики на прийоми у випадках, коли послідовність, яку потрібно створити, є більш складною, або я просто б'ю мертвого коня зі С ++ і справді корутини - єдиний спосіб для загальності?
Ной Уоткінс

3
@NoahWatkins: спрощення спрощує виконання, коли мови підтримують їх. На жаль, C ++ цього немає, тому ітерація простіша. Якщо вам дійсно потрібні супровідні програми, вам фактично потрібна повна роздута нитка, щоб утримувати "стек" вашої функціональної дзвінки на стороні. Безперечно відкривати таку банку з глистами саме для цього в цьому прикладі, але ваш пробіг може змінюватися залежно від ваших реальних потреб.
Матьє М.

1
Реалізація кореневої програми, що не працює на потоці, доступна у Boost boost.org/doc/libs/1_57_0/libs/coroutine/doc/html/index.html із пропозицією щодо стандартизації тут: open-std.org/jtc1/sc22/ wg21 / docs / paper / 2014 / n3985.pdf
boycy

2
@boycy: Насправді є кілька пропозицій щодо процедур, зокрема одна стоса менше, а інша стопка. Це міцний горіх, щоб тріснути, тому я зараз чекаю. Тим часом, все, що не містить стеків, виконуються поряд безпосередньо безпосередньо як вхідні ітератори (просто без цукру).
Матьє М.

3
І все ж подібні, ітератори не такі, як генератори.
Огень Шобајий

52

У C ++ є ітератори, але реалізація ітератора не є простою: потрібно проконсультуватися з концепціями ітератора та ретельно розробити новий клас ітератора для їх реалізації. На щастя, Boost має шаблон iterator_facade, який повинен допомогти реалізувати ітератори та сумісні з ітератором генератори.

Іноді для запуску ітератора може бути використана безпрограма .

PS Дивіться також цю статтю, в якій згадується як switchхак Крістофера М. Колхоффа, так і Boost.Coroutine Олівера Ковальке. Робота Олівера Kowalke в це період спостереження на Boost.Coroutine Джованні П. Deretta.

PS Я думаю, ви також можете написати своєрідний генератор з лямбдами :

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;

PS Ось генератор, реалізований за допомогою процедур Мордор :

#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

Оскільки 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конструктору.


Зауважте, що Coroutine2 вимагає c ++ 11, для якого візуальна студія 2013 недостатня, оскільки підтримується лише частково.
Вестон

5

Усі відповіді, які стосуються написання власного ітератора, абсолютно невірні. Такі відповіді повністю пропускають суть генераторів Python (одна з найбільших та унікальних особливостей мови). Найважливіше в генераторах - це те, що виконання підбирається там, де воно припиняється. Це не трапляється з ітераторами. Натомість вам потрібно вручну зберігати інформацію про стан так, що коли оператор ++ або оператор * викликається заново, потрібна інформація знаходиться на самому початку наступного виклику функції. Ось чому написання власного ітератора C ++ - це гігантський біль; тоді як генератори елегантні та легко читати + писати.

Я не думаю, що є хороший аналог для генераторів Python у рідному C ++, принаймні, поки що (є слух, що врожайність приземлиться в C ++ 17 ). Ви можете отримати щось подібне, звернувшись до сторонньої сторони (наприклад, пропозиція про підсилення Yongwei) або прокатуючи власну.

Я б сказав, що найближче в рідному 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.
  4. Окрім 2, є й інші біти котлоагрегату, які потрібні щоразу, коли ви бажаєте "інстанціювати" об'єкт генератора:
    1. Параметр каналу * вихід
    2. Додаткові змінні в основному: пари, генератор

Ви плутаєте синтаксис з функціональністю. Кілька відповідей вище фактично дозволяють C ++ підбирати виконання з того місця, де воно припинено під час останнього дзвінка. Це ніщо магічне. Справді, Python буде реалізований в C, тому все , що можливо в Python можна в C, хоча і не так зручно.
Еді

@edy Хіба це вже не розглянуто в першому абзаці? Він не стверджує, що еквівалентну функціональність не можна створити в звичайних C ++, тільки що це "гігантський біль".
Кайтайн

@Kaitain Питання тут полягає не в тому, чи потрібно робити генератор в C ++, а в тому, чи існує така модель. Його твердження, що підхід "пропускає суть", що "найближча річ" - це нитки ... просто вводять в оману. Це біль? Можна було прочитати інші відповіді та вирішити для себе.
Еді

@edy Але це не в кінцевому підсумку є вакуумною точкою, враховуючи, що всі мови, що завершують Тьюрінг, в кінцевому рахунку здатні до однакової функціональності? "Все, що можливо в X, можливо в Y", це гарантується правдою для всіх таких мов, але це, здається, не дуже висвітлює спостереження.
Кайтайн

@Kaitain Саме тому, що всі мови, які закінчують Тьюрінг, нібито повинні мати однакові можливості, тому питання, як реалізувати одну особливість на іншій мові, є законним. Нічого, що Python не може бути досягнуто іншою мовою; питання в ефективності та ремонтопридатності. В обох відношеннях C ++ був би прекрасним (r) вибором.
Еді

4

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

Я думаю, що це саме те, що ви шукаєте. Загалом генератори повинні бути доступні в C ++ 17, оскільки це лише експериментальна функція Microsoft VC.


2

Якщо вам потрібно зробити це лише для відносно невеликої кількості конкретних генераторів, ви можете реалізувати кожен як клас, де дані про члени еквівалентні локальним змінним функції генератора Python. Тоді у вас є наступна функція, яка повертає наступне, що видасть генератор, оновлюючи внутрішній стан так, як це робить.

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

Вам також потрібен певний спосіб зазначення припинення. Якщо те, що ви повертаєте, є "вказівником", і NULL не має бути дійсним значенням виходу, ви можете використовувати вказівник NULL як індикатор припинення. Інакше вам потрібен позадіапазонний сигнал.


1

Щось подібне дуже схоже:

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.


1

Використання 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;
}

0

Щось таке :

Приклад використання:

using ull = unsigned long long;

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

    return 0;
}

Буде надруковано цифри від 0 до 99


0

Що ж, сьогодні я також шукав легкої реалізації колекції під C ++ 11. Насправді я був розчарований, тому що все, що я знайшов, є занадто далеким від таких речей, як генератори пітона, або оператора виходу C # ... або занадто складне.

Мета - зробити колекцію, яка видаватиме свої предмети лише тоді, коли цього буде потрібно.

Я хотів, щоб це було так:

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

Генератор.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

Ця відповідь працює на C (і тому я думаю, що працює і в c ++)

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

Це простий, не об'єктно-орієнтований спосіб імітувати генератор. Це працювало так, як очікувалося для мене.


-1

Так само, як функція імітує концепцію стека, генератори моделюють концепцію черги. Решта - семантика.

Як бічна примітка, ви завжди можете імітувати чергу зі стеком, використовуючи стек операцій замість даних. Це практично означає, що ви можете реалізувати поведінку, подібну до черги, повертаючи пару, друге значення якої або має наступну функцію, яку потрібно викликати, або вказує на те, що ми не значилися. Але це більш загально, ніж те, що робить врожайність проти прибутку. Це дозволяє імітувати чергу будь-яких значень, а не однорідних значень, які ви очікуєте від генератора, але без збереження повної внутрішньої черги.

Більш конкретно, оскільки C ++ не має природних абстракцій для черги, вам потрібно використовувати конструкції, які реалізують чергу всередині. Тож відповідь, яка наводила приклад із ітераторами, - це гідна реалізація концепції.

Це практично означає, що ви можете реалізувати щось із функцією черги з голими кістками, якщо просто хочете щось швидко, а потім споживаєте значення черги так само, як ви б споживали значення, отримані від генератора.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.