Evgenii Legotckoi
Там. 22, 2019, 1:42 Т.Қ.

C++17 - Ауыр функцияны бағалау нәтижелерін кэштеумен жалқау үлгілік функтор

Развивая идею кэширования результата вычислений тяжелых функций , предлагаю написать небольшой класс-шаблон, который будет принимать в качестве аргумента функцию, а именно лямбда-функция, как наиболее универсальный инструмент, в рамках которого будет выполняться тяжелая функция.

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


Вступление

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

Я постараюсь показать пример, где это может быть полезно.

Например, в этом искусственном example_, который явно требует рефакторинга, есть ветки, когда тяжелую функцию нужно вызывать несколько раз, когда только один раз, а когда вообще не нужно вызывать.

  1. int calc(int cases, int cases_2, int base)
  2. {
  3. int result = 0;
  4.  
  5. switch (cases)
  6. {
  7. case 0:
  8. {
  9. result = 3 * heavy_calc(base);
  10. break;
  11. }
  12. case 1:
  13. {
  14. result = 4;
  15. break;
  16. }
  17. case 2:
  18. {
  19. result = 2 * heavy_calc(base);
  20. break;
  21. }
  22. case 3:
  23. {
  24. result = 3 * heavy_calc(base);
  25. if (cases_2 < 2)
  26. {
  27. result += heavy_calc(base) / 2;
  28. }
  29. break;
  30. }
  31. default:
  32. return heavy_calc(base);
  33. }
  34.  
  35. switch (cases_2)
  36. {
  37. case 0:
  38. {
  39. result = result * heavy_calc(base);
  40. break;
  41. }
  42. case 1:
  43. {
  44. result += result;
  45. break;
  46. }
  47. case 2:
  48. {
  49. return result - 1;
  50. }
  51. default:
  52. return 2 * heavy_calc(base);
  53. }
  54. return result;
  55. }

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

Ленивый функтор шаблонов с кэшированием результатов

Для этого я предлагаю написать класс-шаблон функтора.

ВНИМАНИЕ!!! Этот код работает только при использовании стандартного C++ 17 или новее

  1. #ifndef LAZYCACHEDFUNCTION_H
  2. #define LAZYCACHEDFUNCTION_H
  3.  
  4. #include <type_traits>
  5.  
  6. // The template, as a template parameter, will take the type of function to be called
  7. // and will print the return type via std :: result_of_t from the C++17 standard
  8. template <typename T, typename CachedType = typename std::result_of_t<T()>>
  9. class LazyCachedFunction
  10. {
  11. public:
  12. // The constructor of the functor takes as argument the function to be called
  13.     // and move it to the m_function member
  14. template<typename T>
  15. explicit inline LazyCachedFunction(T&& function) :
  16. m_function(std::forward<T>(function))
  17. {
  18. }
  19.  
  20. // Do the operator overload of parentheses,
  21.     // so that calling a function inside a functor is like calling a regular function
  22. auto operator()() const
  23. {
  24. // Check that the cache exists
  25. if (!m_initialized)
  26. {
  27. // if not, then call the heavy function
  28. m_value = m_function();
  29. m_initialized = true;
  30. }
  31. // We return the result of calculations from the cache
  32. return m_value;
  33. }
  34.  
  35. private:
  36. T m_function; ///< function called
  37. mutable CachedType m_value; ///< cached result
  38. mutable bool m_initialized {false}; ///< initialization flag, which indicates that the cache is full
  39. };
  40.  
  41. #endif // LAZYCACHEDFUNCTION_H
  42.  

Пример

И теперь мы используем этот функтор

  1. #include <iostream>
  2. #include <string>
  3.  
  4. #include "lazycachedfunction.h"
  5.  
  6. using namespace std;
  7.  
  8. int heavy_calc(int base)
  9. {
  10. cout << "heavy_calc" << std::endl;
  11. // sleep(+100500 years)
  12. return base * 25;
  13. }
  14.  
  15. void calclulateValue(int base)
  16. {
  17. // Wrap heavy function i lambda function
  18.     // this is the most convenient universal option
  19. auto foo = [&]()
  20. {
  21. return heavy_calc(base);
  22. };
  23. // We need to pass the function type to the template parameter, so we need to use decltype
  24. LazyCachedFunction<decltype(foo)> lazyCall(foo);
  25. // Next, call lazyCall() twice and see that the heavy function is called only once
  26. int fooFoo = lazyCall() + lazyCall();
  27. cout << fooFoo << std::endl;
  28. }
  29.  
  30. int main()
  31. {
  32. // Let's double check by double calling calclulateValue
  33. calclulateValue(1);
  34. calclulateValue(10);
  35. return 0;
  36. }
  37.  

Консольный вывод

  1. heavy_calc
  2. 50
  3. heavy_calc
  4. 500

Проверяем, что функция не вызывается, если мы не вызываем оператор скобок в функторе

Для такой проверки мы можем изменить функцию calclulateValue следующим образом

  1. void calclulateValue(int base)
  2. {
  3. auto foo = [&]()
  4. {
  5. return heavy_calc(base);
  6. };
  7. LazyCachedFunction<decltype(foo)> lazyCall(foo);
  8. int fooFoo = 10;
  9. cout << fooFoo << std::endl;
  10. }

Вывод в консоль будет следующим

  1. 10
  2. 10

Что доказывает, что функция не вызывается, когда она не требуется.

Соответственно, в случае, когда не всегда нужно выполнять какие-то тяжелые вычисления, такой функтор может несколько оптимизировать программу.
Не сильно затрагивая структуру метода, который был написан ранее, он также поможет сохранить читабельность кода.

Ол саған ұнайды ма? Әлеуметтік желілерде бөлісіңіз!

Пікірлер

Тек рұқсаты бар пайдаланушылар ғана пікір қалдыра алады.
Кіріңіз немесе Тіркеліңіз