Ленькая оценка в C ++
У C ++ нет встроенной поддержки для ленивой оценки (как это делает Haskell).
Мне интересно, можно ли реализовать ленивую оценку на C ++ разумным образом. Если да, как бы вы это сделали?
EDIT: Мне нравится ответ Конрада Рудольфа.
- Альтернатива Python xrange для R ИЛИ как петляться над большим набором данных lazilly?
- Объясните ленивую оценочную причуду
- Рекурсивная функция, вызывающая переполнение стека
- Прямые ссылки - почему этот код компилируется?
- Передавать аргументы функции dplyr
Мне интересно, можно ли реализовать его более универсальным образом, например, используя параметризованный class lazy, который по существу работает для T, как matrix-matrix работает для матрицы.
Любая операция на T вернется вместо лени. Единственная проблема – хранить аргументы и код операции внутри самого ленивого. Может ли кто-нибудь увидеть, как улучшить это?
- `def` vs` val` vs `lazy val` в Scala
- Почему ленивая оценка полезна?
- Когда следует использовать Lazy ?
- Ленькая оценка в Python
- Недостаточная производительность Haskell foldl с (++)
- Какова (скрытая) стоимость ленивого значения Scala?
- Передача имени переменной в функцию из R
- hibernate: LazyInitializationException: не удалось инициализировать прокси-сервер
Мне интересно, можно ли реализовать ленивую оценку на 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()
метода operator matrix()
которая является оператором неявного преобразования от matrix_add
к простой matrix
. Таким образом, вы можете объединить несколько операций (конечно, обеспечивая соответствующие перегрузки). Оценка выполняется только тогда, когда конечный результат присваивается экземпляру matrix
.
EDIT Я должен был быть более явным. Как бы то ни было, код не имеет смысла, потому что, хотя оценка происходит лениво, все равно происходит в одном выражении. В частности, другое дополнение будет оценивать этот код, если структура matrix_add
будет изменена, чтобы обеспечить добавление в цепочку. C ++ 0x значительно облегчает это, разрешая вариационные шаблоны (т. Е. Списки шаблонов переменной длины).
Тем не менее, один очень простой случай, когда этот код действительно имеет реальную прямую выгоду, таков:
int value = (A + B)(2, 3);
Здесь предполагается, что A
и B
являются двумерными matrixми и что разыменование происходит в записи Fortran, т. Е. Выше вычисляется один элемент из суммы матрицы. Разумеется, расточительно добавлять все матрицы. 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); } };
Другие примеры изобилуют. Я только что вспомнил, что недавно я реализовал что-то, что было связано. В принципе, мне пришлось реализовать class строк, который должен придерживаться фиксированного заранее определенного интерфейса. Однако мой конкретный class строк касался огромных строк, которые на самом деле не были сохранены в памяти. Обычно пользователь будет просто получать небольшие подстроки из исходной строки с помощью функции infix
. Я перегрузил эту функцию для своего строкового типа, чтобы вернуть прокси-сервер, содержащий ссылку на мою строку, а также желаемую начальную и конечную позицию. Только когда эта подстрока была фактически использована, она запросила API C для извлечения этой части строки.
Boost.Lambda очень приятная, но Boost.Proto – именно то , что вы ищете. У него уже есть перегрузки всех операторов C ++, которые по умолчанию выполняют свою обычную функцию при вызове proto::eval()
, но могут быть изменены.
То, что Конрад уже объяснил, можно поместить дальше, чтобы поддерживать вложенные вызовы операторов, все выполняемые лениво. В примере Конрада у него есть объект выражения, который может хранить ровно два аргумента, для ровно двух операндов одной операции. Проблема в том, что он будет выполнять только одно подвыражение лениво, что прекрасно объясняет концепцию в ленивой оценке, простую формулировку, но существенно не улучшает производительность. В другом примере также хорошо показано, как можно применить operator()
чтобы добавить только некоторые элементы, используя этот объект выражения. Но для оценки произвольных сложных выражений нам нужен какой-то механизм, который также может сохранить структуру этого. Мы не можем обойти шаблоны, чтобы сделать это. И имя для этого – это expression templates
. Идея состоит в том, что один шаблонный объект выражения может рекурсивно сохранять структуру произвольного подвыражения, например дерево, где операциями являются узлы, а операнды – дочерние узлы. Для очень хорошего объяснения, которое я только что нашел сегодня (через несколько дней после того, как я написал код ниже), см. Здесь .
template 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 AddOp, Point> operator+(AddOp const& lhs, Point const& p) { return AddOp, Point>(lhs, p); } // add expression template with point at the left template AddOp< Point, AddOp > operator+(Point const& p, AddOp const& rhs) { return AddOp< Point, AddOp >(p, rhs); } // add two points, yield a expression template AddOp< Point, Point > operator+(Point const& lhs, Point const& rhs) { return AddOp(lhs, rhs); }
Теперь, если у вас есть
Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 }; p1 + (p2 + p3); // returns AddOp< Point, AddOp >
Теперь вам просто нужно перегрузить operator = и добавить подходящий конструктор для типа Point и принять AddOp. Измените его определение на:
struct Point { int x, y; Point(int x = 0, int y = 0):x(x), y(y) { } template Point(AddOp const& op) { x = op.get_x(); y = op.get_y(); } template Point& operator=(AddOp 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. Это могла быть большая matrix с множеством полей. Но в то время, когда результат нужен, мы вычисляем его лениво .
Мне нечего добавить в сообщение Konrad, но вы можете взглянуть на Eigen на пример ленивой оценки, сделанной правильно, в приложении реального мира. Это впечатляющий страх.
Я думаю о внедрении classа шаблона, который использует std::function
. Класс должен, более или менее, выглядеть следующим образом:
template class Lazy { public: Lazy(std::function 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 _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 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)
не будет вызываться до тех пор, пока не будет доступна переменная.
Тем не менее этот class далек от совершенства. Первым делом будет конструктор по умолчанию Value
который нужно будет вызывать при инициализации члена (в этом случае печатать Noisy(0)
). _value
этого мы можем использовать указатель для _value
, но я не уверен, повлияет ли это на производительность.
Ответ Йоханнеса работает. Но когда дело доходит до большего числа круглых скобок, это не работает как желание. Вот пример.
Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 }, p4 = { 7, 8 }; (p1 + p2) + (p3+p4)// it works ,but not lazy enough
Поскольку три перегруженных + оператора не охватывали случай
AddOp+AddOp
Поэтому компилятор должен преобразовать либо (p1 + p2), либо (p3 + p4) в Point, что недостаточно лениво. И когда компилятор решает, что делать, он жалуется. Потому что никто не лучше другого. Вот мое расширение: добавьте еще один перегруженный оператор +
template AddOp, AddOp> operator+(const AddOp & leftOperandconst, const AddOp & rightOperand) { return AddOp, AddOp>(leftOperandconst, rightOperand); }
Теперь, компилятор может правильно обработать дело, и не подразумевается преобразование, volia!
C ++ 0x – это хорошо и все … но для тех из нас, кто живет в настоящем, у вас есть библиотека ломбарда Boost и Boost Phoenix. И с целью доведения большого количества функционального программирования до C ++.
Все возможно.
Это зависит от того, что вы имеете в виду:
class X { public: static X& getObjectA() { static X instanceA; return instanceA; } };
Здесь мы имеем влияние глобальной переменной, которая лениво оценивается в момент первого использования.
Как недавно запросили в вопросе.
И украсть Konrad Rudolph и расширить его.
ЛАЗОВЫЙ объект:
template 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 Lazy operator+(T1 const& lhs,T2 const& rhs) { return Lazy(lhs,rhs); } template Lazy operator-(T1 const& lhs,T2 const& rhs) { return Lazy(lhs,rhs); } }
Как это будет сделано в C ++ 0x , с помощью lambda-выражений.
В C ++ 11 ленивая оценка, подобная ответу hiapay, может быть достигнута с помощью std :: shared_future. Вы все еще должны инкапсулировать вычисления в lambdaсе, но позаботиться о нем:
std::shared_future a = std::async(std::launch::deferred, [](){ return 1+1; });
Вот полный пример:
#include #include #define LAZY(EXPR, ...) std::async(std::launch::deferred, [__VA_ARGS__](){ std::cout << "evaluating "#EXPR << std::endl; return EXPR; }) int main() { std::shared_future f1 = LAZY(8); std::shared_future f2 = LAZY(2); std::shared_future 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; }
Достаточно просто создать свой собственный «контейнерный» class, который принимает объект функции генерации и предоставляет iteratorы.