Развивая идею кэширования результата вычислений тяжелых функций , предлагаю написать небольшой класс-шаблон, который будет принимать в качестве аргумента функцию, а именно лямбда-функция, как наиболее универсальный инструмент, в рамках которого будет выполняться тяжелая функция.
В этом случае результат работы функции будет вычисляться не в момент создания Функтора, а в момент вызова операторной скобки () . И при этом результат будет кэшироваться. Это позволит не вызывать тяжелую функцию более одного раза в рамках выполнения метода.
Вступление
Конечно, можно сначала выполнить тяжелую функцию и сохранить результат в переменную, но если логика вашей программы не требует сразу вычислять нужное значение, а только если оно действительно нужно. Тогда вообще нет необходимости вызывать эту функцию.
Я постараюсь показать пример, где это может быть полезно.
Например, в этом искусственном 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
Что доказывает, что функция не вызывается, когда она не требуется.
Соответственно, в случае, когда не всегда нужно выполнять какие-то тяжелые вычисления, такой функтор может несколько оптимизировать программу.
Не сильно затрагивая структуру метода, который был написан ранее, он также поможет сохранить читабельность кода.