Ленивая оценка в C ++ - PullRequest
       44

Ленивая оценка в C ++

54 голосов
/ 05 января 2009

C ++ не имеет встроенной поддержки для отложенной оценки (как Haskell).

Мне интересно, возможно ли реализовать ленивую оценку в C ++ разумным способом. Если да, как бы ты это сделал?

РЕДАКТИРОВАТЬ: Мне нравится ответ Конрада Рудольфа.

Мне интересно, возможно ли реализовать его более общим способом, например, с помощью параметризованного класса lazy, который по существу работает для T так, как matrix_add работает для матрицы.

Любая операция на Т вернет ленивый. Единственная проблема - хранить аргументы и код операции внутри самого lazy. Кто-нибудь может увидеть, как это улучшить?

Ответы [ 12 ]

84 голосов
/ 05 января 2009

Мне интересно, можно ли разумно реализовать ленивые вычисления в 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);
    }
};

Других примеров предостаточно. Я только что вспомнил, что реализовал нечто связанное не так давно. По сути, мне пришлось реализовать строковый класс, который должен придерживаться фиксированного, предварительно определенного интерфейса. Тем не менее, мой конкретный строковый класс имел дело с огромными строками, которые фактически не хранились в памяти. Обычно пользователь просто получает доступ к небольшим подстрокам из исходной строки, используя функцию infix. Я перегрузил эту функцию для моего строкового типа, чтобы он возвращал прокси, который содержал ссылку на мою строку вместе с желаемой начальной и конечной позицией. Только когда эта подстрока фактически использовалась, он запрашивал API C для получения этой части строки.

30 голосов
/ 08 января 2009

Boost.Lambda - это очень хорошо, но Boost.Proto - это точно , что вы ищете. Он уже имеет перегрузки всех операторов C ++, которые по умолчанию выполняют свою обычную функцию при вызове proto::eval(), но могут быть изменены.

25 голосов
/ 05 января 2009

То, что уже объяснил Конрад, можно далее использовать для поддержки вложенных вызовов операторов, все выполняемые лениво. В примере Конрада у него есть объект выражения, который может хранить ровно два аргумента, ровно для двух операндов одной операции. Проблема в том, что он будет выполнять только одно подвыражение лениво, что приятно объясняет концепцию ленивого вычисления, выраженную простыми терминами, но существенно не повышает производительность. Другой пример также хорошо показывает, как можно применить operator(), чтобы добавить только некоторые элементы, используя этот объект выражения. Но чтобы оценить произвольные сложные выражения, нам нужен механизм, который может хранить структуру этого тоже. Мы не можем обойти шаблоны для этого. И имя для этого - expression templates. Идея состоит в том, что один шаблонный объект выражения может рекурсивно хранить структуру некоторого произвольного подвыражения, например, дерево, где операции - это узлы, а операнды - это дочерние узлы. Для очень хорошего объяснения, которое я только что нашел сегодня (через несколько дней после того, как написал код ниже), см. здесь .

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

Это будет хранить любую операцию сложения, даже вложенную, как видно из следующего определения оператора + для простого типа точки:

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. Это могла быть большая матрица со многими полями. Но в то время, когда нужен результат, мы вычисляем его лениво .

10 голосов
/ 06 января 2009

Мне нечего добавить к сообщению Конрада, но вы можете посмотреть на Eigen для примера ленивой оценки, выполненной правильно, в реальном приложении. Это очень внушает благоговение.

3 голосов
/ 08 декабря 2014

Ответ Йоханнеса работает. Но когда дело доходит до нескольких скобок, он не работает, как хотелось бы. Вот пример.

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

}

Теперь компилятор может корректно обрабатывать приведенный выше случай, без неявного преобразования, volia!

3 голосов
/ 18 июля 2013

Я думаю о реализации шаблонного класса, который использует 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, но я не уверен, повлияет ли это на производительность.

2 голосов
/ 05 января 2009

C ++ 0x хорош и все .... но для тех из нас, кто живет в настоящем, у вас есть библиотека Boost lambda и Boost Phoenix. Оба с намерением принести большое количество функционального программирования на C ++.

2 голосов
/ 05 января 2009

Все возможно.

Это зависит от того, что вы имеете в виду:

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

          return instanceA;
     }
};

Здесь мы имеем влияние глобальной переменной, которая лениво оценивается в момент первого использования.

В соответствии с новым запросом в вопросе.
И ворует дизайн Конрада Рудольфа и расширяет его.

Ленивый объект:

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);
    }
}
1 голос
/ 01 декабря 2016

В 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;
}
1 голос
/ 05 января 2009

Как это будет сделано в C ++ 0x с помощью лямбда-выражений.

...