Вычисления по короткой схеме vs ленивые вычисления Ч1



Вчера Евгений поднял тему различий short circuited evaluation и lazy evaluation. Действительно важная тема для обсуждений. Потому что многие путают. Это конечно почти ни на что не влияет, но прояснить все же нужно.



Разберемся с ленивыми вычислениями. Если описать этот термин лозунгом, то это будет что-то типа "вычисли меня, когда я буду нужен". Для плюсов это ненативный термин. Потому что это strict language. В таких языках, например, все параметры функции должны быть вычислены до входа в функцию. Однако, есть языки(Scala, Haskell), где такого условия нет и аргумент вычисляется только тогда, когда он нужен в теле функции. Это значит, что функция может выдать результат, даже не вычислив всех аргументов! То есть вычисление аргумента откладывается до того момента, когда его значение будет нужно.



Но такие вычисления применимы не только к вычислению аргументов, естественно. Чтобы было понятно, давайте приведу несколько примеров, но в плюсовом контексте.



Логгер-синглтон. Не хотим мы запариваться над сложным и безопасным логгером, поэтому напишем его очень просто и безобразно:



class Logger{

Logger() {}

public:

static Logger * instance() {

static Logger myinstance;

return &myinstance;

}

void Log(const std::string str) {

std::cout << str << std::endl;

}

};




Обратите внимание, что конструктор у класса приватный, поэтому вы можете создать объект только через статический метод instance. И объект на самом деле создастся только тогда, когда он вам понадобится и вы вызовете метод instance. Статические переменные функций хоть и имеют глобальный скоуп, но создаются только при первом вызове конкретной функции, когда она стала нужна в коде.



Можно чуть сложнее придумать:



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;

};



struct ZeroDimentionalTensor

{

ZeroDimentionalTensor(int n) : a{n} {}

int a;

};



using ZDTensor = ZeroDimentionalTensor;



struct ZDTensorAdd

{

using Result = ZDTensor;

Result operator()(ZDTensor const& lhs,ZDTensor const& rhs) const

{

Result r = lhs.a + rhs.a;

std::cout << "Actual calculation" << std::endl;

return r;

}

};



Lazy<ZDTensorAdd,ZDTensor,ZDTensor> operator+(ZDTensor const& lhs,ZDTensor const& rhs)

{

return Lazy<ZDTensorAdd,ZDTensor,ZDTensor>(lhs,rhs);

}



int main() {

ZDTensor a{1};

ZDTensor b{2};

std::cout << "Check" << std::endl;

auto sum = a + b;

std::cout << "Check" << std::endl;

bool external_parameter = true;

ZDTensor c{0};

if (std::cout << "Condition" << std::endl; external_parameter) {

c = sum;

} else {

c = 5;

}

std::cout << c.a << std::endl;

}




Здесь мы определяем ленивую структурку, которая может выполнять какую-то бинарную операцию над какими-то объектами. Причем делает она это в операторе преобразования к типу результата бинарной операции. Это довольно важный момент. Пример и так довольно сложный, поэтому не буду мудрить с compile-time проверками правильности типов.



Дальше с помощью этой структурки я хочу лениво посчитать сумму тензоров нулевой размерности(просто посчитать сумму чиселок - не комильфо, а так я как будто бы математик в 10-м поколении).



Создаю совсем простенький класс этого тензора. А затем класс операции сложения тензоров. Это по факту функтор, в котором дополнительно хранится тип результата, и который выполняет реальное сложение тензоров при вызове оператора() с двумя тензорами. Для проверки момента реальных вычислений я вставил консольный вывод



И вишенка на торте - перегружаю оператор+ для того, чтобы он на сложение двух тензоров возвращал не тензор, а нашу ленивую структурку.



Ну и теперь магия в действии. ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ.



Differentiate the meanings of things. Stay cool.



#template #cppcore #cpp17