developer tip

C ++의 지연 평가

copycodes 2020. 12. 28. 08:25
반응형

C ++의 지연 평가


C ++에는 지연 평가에 대한 기본 지원이 없습니다 (하스켈이 지원하는 것처럼).

합리적인 방식으로 C ++에서 지연 평가를 구현할 수 있는지 궁금합니다. 그렇다면 어떻게 하시겠습니까?

편집 : 나는 Konrad Rudolph의 대답을 좋아합니다.

예를 들어 matrix_add가 매트릭스에 대해 작동하는 방식으로 T에서 본질적으로 작동하는 매개 변수화 된 클래스 lazy를 사용하여보다 일반적인 방식으로 구현할 수 있는지 궁금합니다.

T에 대한 모든 작업은 대신 lazy를 반환합니다. 유일한 문제는 lazy 자체 내부에 인수와 연산 코드를 저장하는 것입니다. 누구든지 이것을 개선하는 방법을 볼 수 있습니까?


합리적인 방식으로 C ++에서 지연 평가를 구현할 수 있는지 궁금합니다. 그렇다면 어떻게 하시겠습니까?

예, 이것은 가능하고 매우 자주 수행됩니다 (예 : 행렬 계산). 이를 용이하게하는 주요 메커니즘은 운영자 과부하입니다. 행렬 추가의 경우를 고려하십시오. 함수의 시그니처는 일반적으로 다음과 같습니다.

matrix operator +(matrix const& a, matrix const& b);

이제이 함수를 지연 시키려면 실제 결과 대신 프록시를 반환하는 것으로 충분합니다.

struct matrix_add;

matrix_add operator +(matrix const& a, matrix const& b) {
    return matrix_add(a, b);
}

이제해야 할 일은이 프록시를 작성하는 것입니다.

struct matrix_add {
    matrix_add(matrix const& a, matrix const& b) : a(a), b(b) { }

    operator matrix() const {
        matrix result;
        // Do the addition.
        return result;
    }
private:
    matrix const& a, b;
};

마법은 operator matrix()묵시적 변환 연산자 matrix_add를 일반으로 변환 하는 방법에 있습니다 matrix. 이러한 방식으로 여러 작업을 연결할 수 있습니다 (물론 적절한 오버로드를 제공함으로써). 평가는 최종 결과가 matrix인스턴스에 할당 된 경우에만 발생합니다 .

편집 나는 더 명시 적이어야했다. 그대로 평가가 느리게 발생하지만 여전히 동일한 표현식에서 발생하기 때문에 코드는 의미가 없습니다. 특히, matrix_add구조가 연결 추가를 허용하도록 변경 되지 않는 한 다른 추가는이 코드를 평가합니다 . C ++ 0x는 가변 템플릿 (즉, 가변 길이의 템플릿 목록)을 허용함으로써이를 매우 용이하게합니다.

그러나이 코드가 실제로 실질적이고 직접적인 이점을 갖는 매우 간단한 경우는 다음과 같습니다.

int value = (A + B)(2, 3);

여기서, 있다고 가정 A하고 B이차원 행렬이고 그 역 참조는 포트란 표기법 즉 상기 계산 완료 행렬 요소의 합계를. 물론 전체 행렬을 추가하는 것은 낭비입니다. matrix_add구출 :

struct matrix_add {
    // … yadda, yadda, yadda …

    int operator ()(unsigned int x, unsigned int y) {
        // Calculate *just one* element:
        return a(x, y) + b(x, y);
    }
};

다른 예도 많습니다. 얼마 전에 관련된 무언가를 구현했다는 것을 방금 기억했습니다. 기본적으로 미리 정의 된 고정 인터페이스를 준수해야하는 문자열 클래스를 구현해야했습니다. 그러나 내 특정 문자열 클래스는 실제로 메모리에 저장되지 않은 거대한 문자열을 처리했습니다. 일반적으로 사용자는 function을 사용하여 원래 문자열에서 작은 부분 문자열에 액세스합니다 infix. 내 문자열 유형에 대해이 함수를 오버로드하여 원하는 시작 및 끝 위치와 함께 내 문자열에 대한 참조를 보유한 프록시를 반환했습니다. 이 부분 문자열이 실제로 사용되었을 때만 C API를 쿼리하여 문자열의이 부분을 검색했습니다.


Boost.Lambda는 매우 훌륭하지만 Boost.Proto정확히 당신이 찾고있는 것입니다. 이미 모든 C ++ 연산자의 오버로드가 있으며이 연산자는 기본적으로 proto::eval()호출 될 때 일반적인 기능을 수행 하지만 변경할 수 있습니다.


Konrad가 이미 설명한 내용은 모두 느리게 실행되는 연산자의 중첩 된 호출을 지원하기 위해 추가 할 수 있습니다. Konrad의 예에서 그는 한 연산의 정확히 두 개의 피연산자에 대해 정확히 두 개의 인수를 저장할 수있는 표현식 객체를 가지고 있습니다. 문제는 하나의 하위 표현식 느리게 실행한다는 것입니다 . 이는 지연 평가의 개념을 간단한 용어로 잘 설명하지만 성능을 크게 향상 시키지는 않습니다. 다른 예제는 operator()해당 표현식 객체를 사용하여 일부 요소 만 추가 하는 방법을 잘 보여줍니다 . 그러나 임의의 복잡한 표현식을 평가 하려면 그 구조도 저장할 수있는 메커니즘이 필요합니다 . 이를 위해 템플릿을 사용할 수 없습니다. 그리고 그 이름은expression templates. 아이디어는 하나의 템플릿 표현식 객체가 연산이 노드이고 피연산자가 자식 노드 인 트리처럼 임의의 하위 표현식의 구조를 재귀 적으로 저장할 수 있다는 것입니다. A에 대한 매우 좋은 설명 난 그냥 볼 (내가 코드를 아래에 쓴 몇 일 후) 오늘 발견 여기에 .

template<typename Lhs, typename Rhs>
struct AddOp {
    Lhs const& lhs;
    Rhs const& rhs;

    AddOp(Lhs const& lhs, Rhs const& rhs):lhs(lhs), rhs(rhs) {
        // empty body
    }

    Lhs const& get_lhs() const { return lhs; }
    Rhs const& get_rhs() const { return rhs; }
};

간단한 포인트 유형에 대한 operator +의 다음 정의에서 볼 수 있듯이 중첩 된 작업이라도 모든 추가 작업을 저장합니다.

struct Point { int x, y; };

// add expression template with point at the right
template<typename Lhs, typename Rhs> AddOp<AddOp<Lhs, Rhs>, Point> 
operator+(AddOp<Lhs, Rhs> const& lhs, Point const& p) {
    return AddOp<AddOp<Lhs, Rhs>, Point>(lhs, p);
} 

// add expression template with point at the left
template<typename Lhs, typename Rhs> AddOp< Point, AddOp<Lhs, Rhs> > 
operator+(Point const& p, AddOp<Lhs, Rhs> const& rhs) {
    return AddOp< Point, AddOp<Lhs, Rhs> >(p, rhs);
}

// add two points, yield a expression template    
AddOp< Point, Point > 
operator+(Point const& lhs, Point const& rhs) {
    return AddOp<Point, Point>(lhs, rhs);
}

자, 만약 당신이

Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 };
p1 + (p2 + p3); // returns AddOp< Point, AddOp<Point, Point> >

이제 operator =를 오버로드하고 Point 유형에 적합한 생성자를 추가하고 AddOp를 수락하면됩니다. 정의를 다음과 같이 변경하십시오.

struct Point { 
    int x, y; 

    Point(int x = 0, int y = 0):x(x), y(y) { }

    template<typename Lhs, typename Rhs>
    Point(AddOp<Lhs, Rhs> const& op) {
        x = op.get_x();
        y = op.get_y();
    }

    template<typename Lhs, typename Rhs>
    Point& operator=(AddOp<Lhs, Rhs> const& op) {
        x = op.get_x();
        y = op.get_y();
        return *this;
    }

    int get_x() const { return x; }
    int get_y() const { return y; }
};

그리고 적절한 get_x 및 get_y를 AddOp에 멤버 함수로 추가합니다.

int get_x() const {
    return lhs.get_x() + rhs.get_x();
}

int get_y() const {
    return lhs.get_y() + rhs.get_y();
}

Point 유형의 임시 파일을 생성하지 않은 방법에 유의하십시오. 필드가 많은 큰 매트릭스 일 수 있습니다. 그러나 결과가 필요할 때 우리는 그것을 느리게 계산합니다 .


Konrad의 게시물에 추가 할 내용이 없지만 실제 앱에서 올바르게 수행 된 지연 평가의 예는 Eigen 에서 확인할 수 있습니다. 그것은 꽤 경외심을 불러 일으키는 것입니다.


을 사용하는 템플릿 클래스를 구현하는 것에 대해 생각하고 std::function있습니다. 클래스는 대략 다음과 같아야합니다.

template <typename Value>
class Lazy
{
public:
    Lazy(std::function<Value()> function) : _function(function), _evaluated(false) {}

    Value &operator*()  { Evaluate(); return  _value; }
    Value *operator->() { Evaluate(); return &_value; }

private:
    void Evaluate()
    {
        if (!_evaluated)
        {
            _value = _function();
            _evaluated = true;
        }
    }

    std::function<Value()> _function;
    Value _value;
    bool _evaluated;
};

사용 예 :

class Noisy
{
public:
    Noisy(int i = 0) : _i(i)
    {
        std::cout << "Noisy(" << _i << ")"  << std::endl;
    }
    Noisy(const Noisy &that) : _i(that._i)
    {
        std::cout << "Noisy(const Noisy &)" << std::endl;
    }
    ~Noisy()
    {
        std::cout << "~Noisy(" << _i << ")" << std::endl;
    }

    void MakeNoise()
    {
        std::cout << "MakeNoise(" << _i << ")" << std::endl;
    }
private:
    int _i;
};  

int main()
{
    Lazy<Noisy> n = [] () { return Noisy(10); };

    std::cout << "about to make noise" << std::endl;

    n->MakeNoise();
    (*n).MakeNoise();
    auto &nn = *n;
    nn.MakeNoise();
}

위의 코드는 콘솔에 다음 메시지를 생성해야합니다.

Noisy(0)
about to make noise
Noisy(10)
~Noisy(10)
MakeNoise(10)
MakeNoise(10)
MakeNoise(10)
~Noisy(10)

생성자 인쇄 Noisy(10)는 변수에 액세스 할 때까지 호출되지 않습니다.

하지만이 수업은 완벽하지 않습니다. 첫 번째는의 기본 생성자가 Value멤버 초기화 ( Noisy(0)이 경우 인쇄) 에서 호출되어야한다는 것 입니다. _value대신에 포인터를 사용할 수 있지만 성능에 영향을 미칠지 확실하지 않습니다.


Johannes의 대답은 작동하지만 괄호가 더 많으면 원하는대로 작동하지 않습니다. 여기에 예가 있습니다.

Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 }, p4 = { 7, 8 };
(p1 + p2) + (p3+p4)// it works ,but not lazy enough

세 명의 과부하 + 연산자가 케이스를 다루지 않았기 때문에

AddOp<Llhs,Lrhs>+AddOp<Rlhs,Rrhs>

따라서 컴파일러는 (p1 + p2) 또는 (p3 + p4)를 Point로 변환해야합니다. 이는 충분히 게으르지 않습니다. 컴파일러가 변환 할 항목을 결정하면 불평합니다. 다른 것보다 낫지 않기 때문입니다. 여기에 내 확장 기능이 있습니다 : 또 다른 오버로드 된 연산자 추가 +

    template <typename LLhs, typename LRhs, typename RLhs, typename RRhs>
AddOp<AddOp<LLhs, LRhs>, AddOp<RLhs, RRhs>> operator+(const AddOp<LLhs, LRhs> & leftOperandconst, const AddOp<RLhs, RRhs> & rightOperand)
{
    return  AddOp<AddOp<LLhs, LRhs>, AddOp<RLhs, RRhs>>(leftOperandconst, rightOperand);

}

이제 컴파일러는 위의 경우를 올바르게 처리 할 수 ​​있으며 암시 적 변환이 없습니다.


C ++ 0x는 훌륭합니다 ....하지만 현재에 살고있는 우리에게는 Boost lambda 라이브러리와 Boost Phoenix가 있습니다. 둘 다 많은 양의 함수 프로그래밍을 C ++로 가져 오기위한 의도입니다.


무엇이든 가능합니다.

정확히 의미하는 바에 따라 다릅니다.

class X
{
     public: static X& getObjectA()
     {
          static X instanceA;

          return instanceA;
     }
};

여기에 처음 사용할 때 느리게 평가되는 전역 변수의 영향이 있습니다.

질문에서 새로 요청한대로.
그리고 Konrad Rudolph 디자인을 훔치고 확장합니다.

Lazy 객체 :

template<typename O,typename T1,typename T2>
struct Lazy
{
    Lazy(T1 const& l,T2 const& r)
        :lhs(l),rhs(r) {}

    typedef typename O::Result  Result;
    operator Result() const
    {
        O   op;
        return op(lhs,rhs);
    }
    private:
        T1 const&   lhs;
        T2 const&   rhs;
};

사용 방법:

namespace M
{
    class Matrix
    {
    };
    struct MatrixAdd
    {
        typedef Matrix  Result;
        Result operator()(Matrix const& lhs,Matrix const& rhs) const
        {
            Result  r;
            return r;
        }
    };
    struct MatrixSub
    {
        typedef Matrix  Result;
        Result operator()(Matrix const& lhs,Matrix const& rhs) const
        {
            Result  r;
            return r;
        }
    };
    template<typename T1,typename T2>
    Lazy<MatrixAdd,T1,T2> operator+(T1 const& lhs,T2 const& rhs)
    {
        return Lazy<MatrixAdd,T1,T2>(lhs,rhs);
    }
    template<typename T1,typename T2>
    Lazy<MatrixSub,T1,T2> operator-(T1 const& lhs,T2 const& rhs)
    {
        return Lazy<MatrixSub,T1,T2>(lhs,rhs);
    }
}

람다 식에 의해 C ++ 0x 에서 수행 될 것입니다 .


C ++ 11에서 hiapay의 답변과 유사한 지연 평가는 std :: shared_future를 사용하여 얻을 수 있습니다. 여전히 계산을 람다로 캡슐화해야하지만 메모 화가 처리됩니다.

std::shared_future<int> a = std::async(std::launch::deferred, [](){ return 1+1; });

다음은 전체 예입니다.

#include <iostream>
#include <future>

#define LAZY(EXPR, ...) std::async(std::launch::deferred, [__VA_ARGS__](){ std::cout << "evaluating "#EXPR << std::endl; return EXPR; })

int main() {
    std::shared_future<int> f1 = LAZY(8);
    std::shared_future<int> f2 = LAZY(2);
    std::shared_future<int> f3 = LAZY(f1.get() * f2.get(), f1, f2);

    std::cout << "f3 = " << f3.get() << std::endl;
    std::cout << "f2 = " << f2.get() << std::endl;
    std::cout << "f1 = " << f1.get() << std::endl;
    return 0;
}

Haskell을 우리의 영감으로 받아 들여 보자-그것은 핵심에 게으르다. 또한 C #의 Linq가 모나 딕 (urgh-여기에 단어-죄송합니다) 방식으로 열거자를 사용하는 방법을 염두에 두어야합니다. 마지막으로 코 루틴이 프로그래머에게 제공해야하는 것이 무엇인지 명심하십시오. 즉, 계산 단계 (예 : 생산자 소비자)를 서로 분리하는 것입니다. 그리고 코 루틴이 지연 평가와 어떤 관련이 있는지 생각해 보겠습니다.

위의 모든 것은 어떻게 든 관련이있는 것으로 보입니다.

다음으로, "게으른"이 무엇인지에 대한 개인적인 정의를 추출해 보겠습니다.

한 가지 해석은 다음과 같습니다. 계산을 실행 하기 전에 구성 가능한 방식으로 계산을 명시하고 싶습니다 . 전체 솔루션을 구성하는 데 사용하는 일부 부분은 거대한 (때로는 무한한) 데이터 소스를 매우 잘 활용할 수 있으며 전체 계산도 유한 또는 무한 결과를 생성합니다.

구체적으로 몇 가지 코드를 살펴 보겠습니다. 이에 대한 예가 필요합니다! 여기서는 좋은, 게으른 해결책이 있다는 이유로 fizzbuzz "문제"를 예로 선택합니다.

Haskell에서는 다음과 같습니다.

module FizzBuzz
( fb
)
where
fb n =
    fmap merge fizzBuzzAndNumbers
    where
        fizz = cycle ["","","fizz"]
        buzz = cycle ["","","","","buzz"]
        fizzBuzz = zipWith (++) fizz buzz
        fizzBuzzAndNumbers = zip [1..n] fizzBuzz
        merge (x,s) = if length s == 0 then show x else s

Haskell 함수 cycle는 유한 목록의 값을 영원히 반복함으로써 유한 목록에서 무한 목록 (물론 게으른)을 만듭니다. 열성적인 프로그래밍 스타일에서 이런 식으로 작성하면 알람 벨이 울릴 것입니다 (메모리 오버플로, 무한 루프!). 그러나 게으른 언어에서는 그렇지 않습니다. 요령은 게으른 목록이 즉시 계산되지 않는다는 것입니다. 아마 절대. 일반적으로 후속 코드에 필요한만큼만 필요합니다.

where위 블록 의 세 번째 줄 은 또 다른 게으름을 만듭니다 !! 무한리스트 조합에 의해 목록 fizzbuzz단일 두 요소 제조법에 의해 "는 하나의 문자열로 입력하거나리스트에서 문자열 요소를 연결할". 다시 말하지만, 이것이 즉시 평가 되려면 컴퓨터 리소스가 부족해질 때까지 기다려야합니다.

네 번째 줄에서는 [1..n]무한 지연 목록 을 사용하여 유한 지연 목록 구성원의 튜플을 만듭니다 fizzbuzz. 결과는 여전히 게으르다.

우리 fb기능의 본문에서도 열심 할 필요가 없습니다. 전체 함수는 솔루션이 포함 된 목록을 반환하며, 그 자체는 다시 게으 릅니다. fb 50나중에 (부분적으로) 평가할 수있는 계산으로 결과를 생각할 수도 있습니다. 또는 다른 것들과 결합하여 더 큰 (게으른) 평가로 이어집니다.

따라서 "fizzbuzz"의 C ++ 버전을 시작하려면 계산의 부분 단계를 더 큰 계산 비트로 결합하는 방법을 생각해야합니다. 각 단계는 필요에 따라 이전 단계의 데이터를 그립니다.

내 요점 에서 전체 이야기를 볼 수 있습니다 .

다음은 코드의 기본 아이디어입니다.

C # 및 Linq에 차용, 우리는 "발명의"상태, 범용 형 Enumerator보유
- 부분 계산의 현재 값
- 부분 연산의 상태 (우리는 다음 값을 생성 할 수 있으므로)
- 작업자의 기능은 다음을 만들어 state, 다음 값 및 더 많은 데이터가 있는지 또는 열거가 끝났는지 나타내는 부울.

구성 가능하도록하기 위해 Enumerator<T,S>의 힘에 의해 인스턴스 .(점),이 클래스는 또한 다음과 같은 하스켈 타입 클래스 함수에서 차용 포함 FunctorApplicative.

열거 자에 대한 작업자 함수는 항상 다음과 같은 형식입니다. S -> std::tuple<bool,S,T여기서는 S상태를 나타내는 T일반 유형 변수이고 값을 나타내는 일반 유형 변수 인 계산 단계의 결과입니다.

이 모든 것은 이미 Enumerator클래스 정의 의 첫 번째 줄에서 볼 수 있습니다 .

template <class T, class S>
class Enumerator
{
public:
    typedef typename S State_t;
    typedef typename T Value_t;
    typedef std::function<
        std::tuple<bool, State_t, Value_t>
        (const State_t&
            )
    > Worker_t;

    Enumerator(Worker_t worker, State_t s0)
        : m_worker(worker)
        , m_state(s0)
        , m_value{}
    {
    }
    // ...
};

따라서 특정 열거 자 인스턴스를 만드는 데 필요한 모든 것은 작업자 함수를 만들고 초기 상태 Enumerator를 가지고이 두 인수를 사용하여 인스턴스를 만들어야합니다 .

여기 예제-함수 range(first,last)는 유한 한 범위의 값을 생성합니다. 이것은 Haskell 세계의 게으른 목록에 해당합니다.

template <class T>
Enumerator<T, T> range(const T& first, const T& last)
{
    auto finiteRange =
        [first, last](const T& state)
    {
        T v = state;
        T s1 = (state < last) ? (state + 1) : state;
        bool active = state != s1;
        return std::make_tuple(active, s1, v);
    };
    return Enumerator<T,T>(finiteRange, first);
}

그리고이 함수를 사용할 수 있습니다. 예를 들면 다음과 같습니다. auto r1 = range(size_t{1},10);-10 개의 요소가있는 게으른 목록을 만들었습니다!

이제 우리의 "와우"경험에서 빠진 것은 열거자를 구성하는 방법을 보는 것입니다. Haskells cycle함수로 돌아 오면 일종의 멋진 일입니다. C ++ 세계에서는 어떻게 보일까요? 여기있어:

template <class T, class S>
auto
cycle
( Enumerator<T, S> values
) -> Enumerator<T, S>
{
    auto eternally =
        [values](const S& state) -> std::tuple<bool, S, T>
    {
        auto[active, s1, v] = values.step(state);
        if (active)
        {
            return std::make_tuple(active, s1, v);
        }
        else
        {
            return std::make_tuple(true, values.state(), v);
        }
    };
    return Enumerator<T, S>(eternally, values.state());
}

열거자를 입력으로 사용하고 열거자를 반환합니다. 로컬 (람다) 함수 eternally는 값과 값이 부족할 때마다 입력 열거를 시작 값으로 재설정합니다.-우리는 인수로 제공 한 목록의 무한 반복 버전이 auto foo = cycle(range(size_t{1},3));있습니다. : 그리고 우리는 이미 뻔뻔하게 게으른 구성을 할 수 있습니다. " 계산 ".

zip두 개의 입력 열거 자에서 새 열거자를 만들 수도 있다는 것을 보여주는 좋은 예입니다. 결과 열거자는 입력 열거 자 중 더 작은 값만큼 많은 값을 생성합니다 (각 입력 열거 자에 대해 하나씩, 요소가 2 개인 튜플). 내가 구현 한 zip내부 class Enumerator자체. 다음과 같은 모습입니다.

// member function of class Enumerator<S,T> 
template <class T1, class S1>
auto
zip
( Enumerator<T1, S1> other
) -> Enumerator<std::tuple<T, T1>, std::tuple<S, S1> >
{
    auto worker0 = this->m_worker;
    auto worker1 = other.worker();
    auto combine =
        [worker0,worker1](std::tuple<S, S1> state) ->
        std::tuple<bool, std::tuple<S, S1>, std::tuple<T, T1> >
    {
        auto[s0, s1] = state;
        auto[active0, newS0, v0] = worker0(s0);
        auto[active1, newS1, v1] = worker1(s1);
        return std::make_tuple
            ( active0 && active1
            , std::make_tuple(newS0, newS1)
            , std::make_tuple(v0, v1)
            );
    };
    return Enumerator<std::tuple<T, T1>, std::tuple<S, S1> >
        ( combine
        , std::make_tuple(m_state, other.state())
        );
}

"결합"이 어떻게 두 소스의 상태와 두 소스의 값을 결합하는지에 유의하십시오.

이 게시물은 이미 TL; DR입니다. 많은 사람들을 위해 ...

요약

예, 지연 평가는 C ++로 구현할 수 있습니다. 여기에서는 haskell의 함수 이름과 C # 열거 자 및 Linq의 패러다임을 빌려서 수행했습니다. 파이썬 itertools, btw와 유사점이있을 수 있습니다. 비슷한 접근 방식을 따랐다 고 생각합니다.

내 구현 (위의 요점 링크 참조)은 프로덕션 코드가 아닌 프로토 타입입니다. 그래서 내 편에서 어떤 보증도하지 않습니다. 그러나 일반적인 아이디어를 전달하는 데모 코드로도 사용됩니다.

그리고이 대답은 fizzbuz의 최종 C ++ 버전이 없다면 무엇일까요? 여기있어:

std::string fizzbuzz(size_t n)
{
    typedef std::vector<std::string> SVec;
    // merge (x,s) = if length s == 0 then show x else s
    auto merge =
        [](const std::tuple<size_t, std::string> & value)
        -> std::string
    {
        auto[x, s] = value;
        if (s.length() > 0) return s; 
        else return std::to_string(x);
    };

    SVec fizzes{ "","","fizz" };
    SVec buzzes{ "","","","","buzz" };

    return
    range(size_t{ 1 }, n)
    .zip
        ( cycle(iterRange(fizzes.cbegin(), fizzes.cend()))
          .zipWith
            ( std::function(concatStrings)
            , cycle(iterRange(buzzes.cbegin(), buzzes.cend()))
            )
        )
    .map<std::string>(merge)
    .statefulFold<std::ostringstream&>
    (
        [](std::ostringstream& oss, const std::string& s) 
        {
            if (0 == oss.tellp())
            {
                oss << s;
            }
            else
            {
                oss << "," << s;
            }
        }
        , std::ostringstream()
    )
    .str();
}

그리고 ... 포인트를 더 멀리 끌어 올리기 위해-여기 호출자에게 "무한 목록"을 반환하는 fizzbuzz의 변형이 있습니다.

typedef std::vector<std::string> SVec;
static const SVec fizzes{ "","","fizz" };
static const SVec buzzes{ "","","","","buzz" };

auto fizzbuzzInfinite() -> decltype(auto)
{
    // merge (x,s) = if length s == 0 then show x else s
    auto merge =
        [](const std::tuple<size_t, std::string> & value)
        -> std::string
    {
        auto[x, s] = value;
        if (s.length() > 0) return s;
        else return std::to_string(x);
    };

    auto result =
        range(size_t{ 1 })
        .zip
        (cycle(iterRange(fizzes.cbegin(), fizzes.cend()))
            .zipWith
            (std::function(concatStrings)
                , cycle(iterRange(buzzes.cbegin(), buzzes.cend()))
            )
        )
        .map<std::string>(merge)
        ;
    return result;
}

그 함수의 정확한 반환 유형이 무엇인지에 대한 질문을 피하는 방법을 배울 수 있기 때문에 보여줄 가치가 있습니다 (함수 구현, 즉 코드가 열거자를 결합하는 방법에 따라 다름).

또한 그것은 우리가 벡터 이동했다는 것을 보여줍니다 fizzesbuzzes외부 결국 외부에, 게으른 메커니즘이 값을 생성 할 때 주변에 아직도 있도록 기능의 범위를. 그렇게하지 않았다면 iterRange(..)코드는 오랫동안 사라진 벡터에 대한 반복자를 저장했을 것입니다.


값이 필요할 때까지 평가되지 않는 매우 간단한 지연 평가 정의를 사용하여 포인터와 매크로 (구문 설탕의 경우)를 사용하여이를 구현할 수 있다고 말하고 싶습니다.

#include <stdatomic.h>

#define lazy(var_type) lazy_ ## var_type

#define def_lazy_type( var_type ) \
    typedef _Atomic var_type _atomic_ ## var_type; \
    typedef _atomic_ ## var_type * lazy(var_type);  //pointer to atomic type

#define def_lazy_variable(var_type, var_name ) \
    _atomic_ ## var_type _ ## var_name; \
    lazy_ ## var_type var_name = & _ ## var_name;

#define assign_lazy( var_name, val ) atomic_store( & _ ## var_name, val )
#define eval_lazy(var_name) atomic_load( &(*var_name) )

#include <stdio.h>

def_lazy_type(int)

void print_power2 ( lazy(int) i )
{
      printf( "%d\n", eval_lazy(i) * eval_lazy(i) );
}

typedef struct {
    int a;
} simple;

def_lazy_type(simple)

void print_simple ( lazy(simple) s )
{
    simple temp = eval_lazy(s);
    printf("%d\n", temp.a );
}


#define def_lazy_array1( var_type, nElements, var_name ) \
    _atomic_ ## var_type  _ ## var_name [ nElements ]; \
    lazy(var_type) var_name = _ ## var_name; 

int main ( )
{
    //declarations
    def_lazy_variable( int, X )
    def_lazy_variable( simple, Y)
    def_lazy_array1(int,10,Z)
    simple new_simple;

    //first the lazy int
    assign_lazy(X,111);
    print_power2(X);

    //second the lazy struct
    new_simple.a = 555;
    assign_lazy(Y,new_simple);
    print_simple ( Y );

    //third the array of lazy ints
    for(int i=0; i < 10; i++)
    {
        assign_lazy( Z[i], i );
    }

    for(int i=0; i < 10; i++)
    {
        int r = eval_lazy( &Z[i] ); //must pass with &
        printf("%d\n", r );
    }

    return 0;
}

함수 에서 실제로 필요할 때 값을 얻기 위해 포인터를 역 참조하는 것 이상을 수행 print_power2하는 매크로 eval_lazy가 있음을 알 수 있습니다. lazy 유형은 원자 적으로 액세스되므로 스레드로부터 완전히 안전합니다.

ReferenceURL : https://stackoverflow.com/questions/414243/lazy-evaluation-in-c

반응형