Развивая идею кэширования результата вычислений тяжелых функций , предлагаю написать небольшой класс-шаблон, который будет принимать в качестве аргумента функцию, а именно лямбда-функция, как наиболее универсальный инструмент, в рамках которого будет выполняться тяжелая функция.
В этом случае результат работы функции будет вычисляться не в момент создания Функтора, а в момент вызова операторной скобки () . И при этом результат будет кэшироваться. Это позволит не вызывать тяжелую функцию более одного раза в рамках выполнения метода.
Вступление
Конечно, можно сначала выполнить тяжелую функцию и сохранить результат в переменную, но если логика вашей программы не требует сразу вычислять нужное значение, а только если оно действительно нужно. Тогда вообще нет необходимости вызывать эту функцию.
Я постараюсь показать пример, где это может быть полезно.
Например, в этом искусственном example_, который явно требует рефакторинга, есть ветки, когда тяжелую функцию нужно вызывать несколько раз, когда только один раз, а когда вообще не нужно вызывать.
int calc(int cases, int cases_2, int base) { int result = 0; switch (cases) { case 0: { result = 3 * heavy_calc(base); break; } case 1: { result = 4; break; } case 2: { result = 2 * heavy_calc(base); break; } case 3: { result = 3 * heavy_calc(base); if (cases_2 < 2) { result += heavy_calc(base) / 2; } break; } default: return heavy_calc(base); } switch (cases_2) { case 0: { result = result * heavy_calc(base); break; } case 1: { result += result; break; } case 2: { return result - 1; } default: return 2 * heavy_calc(base); } return result; }
Одним из возможных решений было бы вызвать эту функцию один раз в самом начале и сохранить результат в переменной, но тогда мы будем замедлять выполнение кода в тех ветках, где не требуется тяжелый вызов функции. Поэтому стоит подумать, как переписать код, чтобы тяжелая функция вызывалась только один раз.
Ленивый функтор шаблонов с кэшированием результатов
Для этого я предлагаю написать класс-шаблон функтора.
ВНИМАНИЕ!!! Этот код работает только при использовании стандартного C++ 17 или новее
#ifndef LAZYCACHEDFUNCTION_H #define LAZYCACHEDFUNCTION_H #include <type_traits> // The template, as a template parameter, will take the type of function to be called // and will print the return type via std :: result_of_t from the C++17 standard template <typename T, typename CachedType = typename std::result_of_t<T()>> class LazyCachedFunction { public: // The constructor of the functor takes as argument the function to be called // and move it to the m_function member template<typename T> explicit inline LazyCachedFunction(T&& function) : m_function(std::forward<T>(function)) { } // Do the operator overload of parentheses, // so that calling a function inside a functor is like calling a regular function auto operator()() const { // Check that the cache exists if (!m_initialized) { // if not, then call the heavy function m_value = m_function(); m_initialized = true; } // We return the result of calculations from the cache return m_value; } private: T m_function; ///< function called mutable CachedType m_value; ///< cached result mutable bool m_initialized {false}; ///< initialization flag, which indicates that the cache is full }; #endif // LAZYCACHEDFUNCTION_H
Пример
И теперь мы используем этот функтор
#include <iostream> #include <string> #include "lazycachedfunction.h" using namespace std; int heavy_calc(int base) { cout << "heavy_calc" << std::endl; // sleep(+100500 years) return base * 25; } void calclulateValue(int base) { // Wrap heavy function i lambda function // this is the most convenient universal option auto foo = [&]() { return heavy_calc(base); }; // We need to pass the function type to the template parameter, so we need to use decltype LazyCachedFunction<decltype(foo)> lazyCall(foo); // Next, call lazyCall() twice and see that the heavy function is called only once int fooFoo = lazyCall() + lazyCall(); cout << fooFoo << std::endl; } int main() { // Let's double check by double calling calclulateValue calclulateValue(1); calclulateValue(10); return 0; }
Консольный вывод
heavy_calc 50 heavy_calc 500
Проверяем, что функция не вызывается, если мы не вызываем оператор скобок в функторе
Для такой проверки мы можем изменить функцию calclulateValue следующим образом
void calclulateValue(int base) { auto foo = [&]() { return heavy_calc(base); }; LazyCachedFunction<decltype(foo)> lazyCall(foo); int fooFoo = 10; cout << fooFoo << std::endl; }
Вывод в консоль будет следующим
10 10
Что доказывает, что функция не вызывается, когда она не требуется.
Соответственно, в случае, когда не всегда нужно выполнять какие-то тяжелые вычисления, такой функтор может несколько оптимизировать программу.
Не сильно затрагивая структуру метода, который был написан ранее, он также поможет сохранить читабельность кода.