Python 생성기 패턴에 해당하는 C ++


117

C ++에서 모방해야하는 Python 코드의 예가 있습니다. 특정 솔루션 (예 : co-routine 기반 yield 솔루션은 허용 가능한 답변이기는하지만)이 필요하지 않습니다. 단순히 어떤 방식 으로든 의미를 재현하면됩니다.

파이썬

이것은 구체화 된 버전을 저장하기에 너무 큰 기본 시퀀스 생성기입니다.

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 ++에서 해결책을 찾을 수있는 유일한 방법은 yieldC ++ 코 루틴 을 모방 하는 것입니다.하지만이 작업을 수행하는 방법에 대한 좋은 참조를 찾지 못했습니다. 이 문제에 대한 대체 (일반적이지 않은) 솔루션에도 관심이 있습니다. 패스 사이에 시퀀스 사본을 보관할 메모리 예산이 충분하지 않습니다.


여기에서 볼 수 있듯이 stackoverflow.com/questions/3864410/… 코 루틴은 구현하기에 좋은 생각이 아닙니다. 버퍼링 된 읽기로 할 수 없습니까? stackoverflow.com/questions/4685862/...
batbaatar

C ++ 반복자는 이와 같은 것을 지원해야합니다.
Lalaland 2012 년

답변:


79

생성기는 C ++에 다른 이름 인 Input Iterators 바로 아래에 있습니다. 예를 들어,에서 읽는 std::cin것은 char.

생성기가 수행하는 작업을 이해하기 만하면됩니다.

  • 데이터 덩어리가 있습니다. 지역 변수는 상태를 정의합니다.
  • 초기화 방법이 있습니다
  • "다음"방법이 있습니다
  • 종료 신호를 보내는 방법이 있습니다.

사소한 예에서는 충분히 쉽습니다. 개념적으로 :

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
내가 준 질문에 기술적으로 정확하기 때문에 귀하의 답변을 수락했습니다 (감사합니다!). 생성해야 할 시퀀스가 ​​더 복잡한 경우 기술에 대한 포인터가 있습니까? 아니면 여기서 C ++로 죽은 말을 치고 실제로 코 루틴이 일반성을위한 유일한 방법입니까?
Noah Watkins

3
@NoahWatkins : 코 루틴은 언어가 지원할 때 쉽게 구현할 수 있도록합니다. 불행히도 C ++는 그렇지 않으므로 반복이 더 쉽습니다. 코 루틴이 정말로 필요하다면, 함수 호출의 "스택"을 측면에 유지하기 위해 실제로 완전한 스레드가 필요합니다. 이 웜의 개방 등 캔에 과잉 확실히의 바로 이 예에서는 그것에 대해,하지만 귀하의 마일리지가 실제 필요에 따라 다를 수 있습니다.
Matthieu M.

1
스레드 기반이 아닌 코 루틴 구현은 Boost boost.org/doc/libs/1_57_0/libs/coroutine/doc/html/index.html 에서 표준화 제안과 함께 사용할 수 있습니다. open-std.org/jtc1/sc22/ wg21 / docs / papers / 2014 / n3985.pdf
boycy

2
@boycy : 실제로 코 루틴에 대한 여러 제안이 있습니다. 특히 하나는 스택이없고 다른 하나는 스택이 가득합니다. 깨지기 힘든 너트이므로 지금은 기다리고 있습니다. 하지만 그 동안 스택이없는 코 루틴은 입력 반복자 (설탕없이)로 거의 직접 구현할 수 있습니다.
Matthieu M.

3
그러나 유사하지만 반복기는 생성기와 동일하지 않습니다.
Огњен Шобајић

52

C ++에는 반복기가 있지만 반복기를 구현하는 것은 간단하지 않습니다. 반복기 개념 을 참조하고 이를 구현하기 위해 새 반복기 클래스를 신중하게 디자인해야합니다. 고맙게도 Boost에는 반복기 및 반복기 호환 생성기를 구현하는 데 도움 이되는 iterator_facade 템플릿이 있습니다.

때때로 스택리스 코 루틴을 사용하여 반복자를 구현할 수 있습니다 .

PS Christopher M. Kohlhoff 의 해킹과 Oliver Kowalke의 Boost.Coroutine 을 모두 언급하는 이 기사 를 참조하십시오 . 올리버 Kowalke의 작품은 후속작 인Boost.Coroutine 조반니 P. Deretta에 의해.switch

추신 : 람다 로 일종의 생성기 작성할 수도 있다고 생각합니다 .

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 다음은 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;
}

22

이후 Boost.Coroutine2는 지금 매우 잘 지원 (I 정확히 같은 해결 싶었 기 때문에 나는 그것을 발견 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생성자에 전달 될 때 하나의 인수 (of ) 만받는 함수 개체를 생성해야합니다 .


Coroutine2에는 Visual Studio 2013이 부분적으로 만 지원되므로 충분하지 않은 C ++ 11이 필요합니다.
Weston

5

자신의 반복자를 작성하는 것과 관련된 모든 답변은 완전히 잘못되었습니다. 이러한 답변은 Python 생성기 (언어의 가장 크고 독특한 기능 중 하나)의 요점을 완전히 놓칩니다. 제너레이터에서 가장 중요한 점은 실행이 중단 된 부분에서 시작된다는 것입니다. 이것은 반복자에게는 발생하지 않습니다. 대신 operator ++ 또는 operator *가 새로 호출 될 때 올바른 정보가 다음 함수 호출 의 맨 처음 에 제자리 에 있도록 상태 정보를 수동으로 저장해야합니다 . 이것이 바로 C ++ 반복자를 직접 작성하는 것이 엄청난 고통 인 이유입니다. 반면 생성기는 우아하고 읽기와 쓰기가 쉽습니다.

적어도 아직까지는 네이티브 C ++에서 Python 생성기에 대한 좋은 아날로그가 있다고 생각하지 않습니다 ( 수익률이 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. generator.join ()
  3. 이로 인해 발전기를 중지 할 수 없습니다. 이 기능을 추가하기 위해 약간의 수정을 가할 수 있지만 코드가 복잡해집니다. 파이썬의 yield 문만큼 깨끗하지 않을 것입니다.
  4. 2 외에도 생성기 객체를 "인스턴스화"하려고 할 때마다 필요한 상용구의 다른 비트가 있습니다.
    1. 채널 * 출력 매개 변수
    2. 메인의 추가 변수 : 쌍, 생성기

구문과 기능을 혼동하고 있습니다. 위의 몇 가지 답변은 실제로 C ++가 마지막 호출 중에 중단 된 위치에서 실행을 선택할 수 있도록합니다. 마법이 아닙니다. 사실, 파이썬 C 구현 되었기 때문에 파이썬에서 가능한 모든 것이 C에서도 가능하지만 편리하지는 않습니다.
Edy

@edy 이미 첫 번째 단락에서 언급되지 않았습니까? 그는 동등한 기능이 기존의 C ++에서 생성 될 수 없다고 주장하는 것이 아니라 "엄청난 고통"일뿐입니다.
Kaitain

@Kaitain 여기서 질문은 C ++에서 생성기를 수행하는 것이 고통 스러운지 여부가 아니라 그렇게 할 패턴이 있는지 여부입니다. 접근 방식이 "요점을 놓쳤다", "가장 가까운 것"이 스레드라는 그의 주장은 오해의 소지가 있습니다. 고통입니까? 하나는 다른 답변을 읽고 스스로 결정할 수 있습니다.
Edy

@edy 그러나 모든 Turing-complete 언어가 궁극적으로 동일한 기능을 수행 할 수 있다는 점을 감안할 때 이것은 결국 공허한 지점이되지 않습니까? "X에서 가능한 것은 무엇이든 Y에서 가능하다"는 모든 언어에 대해 사실이 보장되지만, 그것은 나에게 그다지 빛나는 관찰이 아닌 것 같습니다.
Kaitain

@Kaitain 정확하게 모든 Turing-complete 언어는 동일한 기능을 가져야하기 때문에 한 기능을 다른 언어로 구현하는 방법에 대한 질문은 합법적입니다. 파이썬이 가지고있는 것은 다른 언어로 성취 될 수 없습니다. 문제는 효율성과 유지 보수성입니다. 두 가지 측면에서 C ++는 좋은 선택이 될 것입니다.
Edy


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

operator ()를 사용하는 것은이 생성기로 무엇을 하려는지에 대한 질문 일뿐입니다. 예를 들어 스트림으로 빌드하고 istream_iterator에 적응하는지 확인할 수도 있습니다.


1

사용 범위-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;
    });

이 게시물을 찾았습니다 .IMHO의 베스트 답변은 Yongwei Wu의 boost.coroutine2에 관한 것 입니다. 저자가 원하는 것에 가장 가깝기 때문에.

부스트 쿠 루틴을 배울 가치가 있습니다. 그리고 저는 아마도 주말에 할 것입니다. 하지만 지금까지는 아주 작은 구현을 사용하고 있습니다. 다른 사람에게 도움이되기를 바랍니다.

다음은 사용 예 및 구현입니다.

예 .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

이 답변은 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 ++에는 큐에 대한 자연스러운 추상화가 없기 때문에 내부적으로 큐를 구현하는 구문을 사용해야합니다. 따라서 반복자를 사용한 예제를 제공 한 대답은 개념의 적절한 구현입니다.

이것이 실질적으로 의미하는 바는 빠르게 무언가를 원하는 경우 베어 본 대기열 기능으로 무언가를 구현 한 다음 생성기에서 산출 된 값을 소비하는 것처럼 대기열의 값을 소비 할 수 있다는 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.