Как отложить вычисления в C ++ до тех пор, пока они не понадобятся?

В C ++ (*), возможно ли иметь структуру, которая откладывает некоторые вычисления до тех пор, пока они не понадобятся (и, возможно, никогда не выполняет вычисления, если в этом нет необходимости)? Мой вариант использования выглядит следующим образом: у меня есть примерно дюжина переменных типа bool, каждая из которых вычисляется с помощью вызова некоторой функции. После этого идет довольно длинный (и сложный) условный оператор, в котором эти переменные типа bool используются в различных комбинациях, чтобы определить, какое действие будет выполнять код следующим.

Вот несколько надуманных примеров кода, которые, надеюсь, лучше иллюстрируют то, что я делаю:

bool const b1 = func1(param1,param2,param3);
bool const b2 = func2(param4);
// ...
bool const b15 = func15(param35,param36,param37,param38);

if (b1 && !b5 && (b2 || b3)) { do_something1(); }
else if (b3 && !b15 || (b4 && b9 && b6)) { do_something2(); }
else if (b14 || b10 || (!b11 && b7)) { do_something3(); }
else if (b8) {
    if (!b1 || !b6) { do_something4(); }
    else if ( /* ... */ ) // ... etc
}
// ... and on and on

Это чисто надуманный пример, но, надеюсь, он иллюстрирует идею.

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

Более того, в условном выражении на любой тип bool можно ссылаться несколько раз; поэтому использование функций напрямую означает, что выполнение может дублироваться. (Я думал, что std :: bind может привести меня туда с точки зрения удобочитаемости; но он все равно потенциально вызовет любой из вызовов funcN () несколько раз.)

Я ищу лучшее из обоих слов, например, отложенное вычисление. Что, если вместо того, чтобы вычисляться и назначаться явно в начале кода, я мог бы сказать, только оценивать их по мере необходимости (и запоминать результат). Большой условный оператор таков, что, как правило, не все логические объекты нужно вычислять, чтобы определить, что будет дальше. Целью здесь является повышение производительности, поскольку этот код вызывается часто. Поэтому я стараюсь сократить объем работы, выполняемой на каждой итерации.

(*) Предпочтительно C ++ 14 (или старше), так как это то, что использует мой работодатель.

Изменить: Как насчет этого:

#include <iostream>
#include <functional>

//////////////////////////////////////////////////////////////////////////////
class Sum
{
    public:
        int sum(int const a, int const b) { ++n_calls_; return (a+b); }
        int getNCalls() const { return n_calls_; }

    private:
        int n_calls_ = 0;
};

//////////////////////////////////////////////////////////////////////////////
template <class BoundFunc, typename RetType>
class DeferredCompute
{
    public:
        DeferredCompute(BoundFunc const& f) : func_(f) { }

        RetType operator()()
        {
            if (!computed_)
            {
                value_ = func_();
                computed_ = true;
            }
            return value_;
        }

    private:
        bool             computed_ = false;
        RetType          value_;
        BoundFunc const& func_;
};

//////////////////////////////////////////////////////////////////////////////
int main(int argc, char* argv[])
{
    Sum s;
    auto boundSum = std::bind(&Sum::sum, &s, 75, 25);

    DeferredCompute<decltype(boundSum), int> deferredSum(boundSum);

    // call function directly repeatedly
    for (int i=0; i<5; ++i)
    {
      std::cout << "boundSum()=" << boundSum() << std::endl;
    }
    std::cout << "s.getNCalls()=" << s.getNCalls() << std::endl;

    // should only call once
    for (int i=0; i<5; ++i)
    {
      std::cout << "deferredSum()=" << deferredSum() << std::endl;
    }
    std::cout << "s.getNCalls()=" << s.getNCalls() << std::endl;

    return 0;
}

Вывод:

boundSum()=100
boundSum()=100
boundSum()=100
boundSum()=100
boundSum()=100
s.getNCalls()=5
deferredSum()=100
deferredSum()=100
deferredSum()=100
deferredSum()=100
deferredSum()=100
s.getNCalls()=6

person Matt    schedule 15.09.2020    source источник
comment
Попробуйте std :: async в режиме deferred   -  person Alan Birtles    schedule 15.09.2020
comment
В базовом языке C ++ вашей библиотеки нет ничего, что могло бы это сделать. Но не так уж сложно реализовать что-то, что выглядело бы точно так же, что-то, что притворяется bool, действует как bool, но вызывает функцию при первой ссылке и использует ранее оцененный результат для всех последующих ссылок.   -  person Sam Varshavchik    schedule 15.09.2020
comment
Возможно ли иметь структуру, которая откладывает некоторые вычисления до тех пор, пока они не понадобятся? да. Если вы его запрограммируете или используете библиотеку, которая его предоставляет.   -  person cmaster - reinstate monica    schedule 15.09.2020
comment
Связанные (хотя и немного старые, но все еще актуальные): stackoverflow.com/questions/414243/lazy-evaluation-in -c   -  person Eljay    schedule 16.09.2020


Ответы (4)


std :: async с опцией std :: launch :: deferred - это то, что вы ищете.

https://en.cppreference.com/w/cpp/thread/async

eg

auto future = std::async(std::launch::deferred, [](){return 5;});
// future isn't calculated yet

auto result = future.get();
// result = 5, and will remain cached while in scope.
person UKMonkey    schedule 15.09.2020
comment
Я не уверен, что это сработает в моем случае, см. std :: async и лямбда-функция в C ++ не дает связанного состояния. В частности, нельзя дважды звонить get() в одном и том же будущем ». Насколько я понимаю, документация get (), будущее за недействителен после первого вызова get(). В моем примере на любой из bools можно ссылаться более одного раза, что подразумевает несколько вызовов future.get (). - person Matt; 18.09.2020
comment
@Matt, так что вы не хотите, чтобы отложить расчет; потому что это то, что функция делает в любом случае - но как кэшировать результат функции, чтобы вы выполняли вычисление только один раз. Это не один и тот же вопрос. - person UKMonkey; 18.09.2020

Сначала я бы попробовал использовать лямбда-замыкания.

const auto b1 = [&]() { return func1(param1,param2,param3); };
const auto b2 = [&]() { return func2(param4); };
// ...
const auto b15 = [&]() { return func15(param35,param36,param37,param38); };

if (b1() && !b5() && (b2() || b3())) { do_something1(); }
...

Если вам нужно кэшировать результаты bool, но не на все время жизни программы (статическое), это решение может сделать это (три уровня лямбда-закрытия; это начало).

/**
  g++ -std=c++17 -o prog_cpp prog_cpp.cpp \
      -pedantic -Wall -Wextra -Wconversion -Wno-sign-conversion \
      -g -O0 -UNDEBUG -fsanitize=address,undefined
**/

#include <iostream>

void
test(int i)
{
  auto cache=[](auto expr)
    {
      return [expr, res=false, done=false]() mutable
        {
          if(!done) { res=expr(); done=true; }
          return res;
        };
    };
  auto b1=cache([&]() { std::cout << "(eval b1)"; return i>2; });
  auto b2=cache([&]() { std::cout << "(eval b2)"; return i<5; });
  std::cout << "1: b1=" << b1() << "  b2=" << b2() << '\n';
  std::cout << "2: b1=" << b1() << "  b2=" << b2() << '\n';
}

int
main()
{
  for(int i=0; i<6; ++i)
  {
    std::cout << "~~~~~~~~\n";
    test(i);
  }
  return 0;
}
/**
~~~~~~~~
1: b1=(eval b1)0  b2=(eval b2)1
2: b1=0  b2=1
~~~~~~~~
1: b1=(eval b1)0  b2=(eval b2)1
2: b1=0  b2=1
~~~~~~~~
1: b1=(eval b1)0  b2=(eval b2)1
2: b1=0  b2=1
~~~~~~~~
1: b1=(eval b1)1  b2=(eval b2)1
2: b1=1  b2=1
~~~~~~~~
1: b1=(eval b1)1  b2=(eval b2)1
2: b1=1  b2=1
~~~~~~~~
1: b1=(eval b1)1  b2=(eval b2)0
2: b1=1  b2=0
**/
person prog-fh    schedule 15.09.2020
comment
Без мемоизации / кеширования вы в конечном итоге будете перезапускать их каждый раз, когда вам понадобится bool. Если вы собираетесь пойти по этому пути, я бы создал класс-оболочку с перегруженным оператором int(), который вызовет функцию только один раз за кулисами. - person scohe001; 15.09.2020

Для удобства чтения и поддержки вы можете организовать программу как конечный автомат. Это дает вам преимущество отделения переходов между состояниями и действий друг от друга, плюс должно быть достаточно просто перемонтировать логику позже, если возникнет необходимость.

См. Здесь несколько примеров: Код C ++ для конечного автомата

person Den-Jason    schedule 15.09.2020

Что, если бы вместо того, чтобы вычисляться и назначаться явно в начале кода, я мог бы сказать, оценивать их только по мере необходимости (и запоминать результат)

/// @brief only evaluate these as needed (and remember the result)
class lazy final
{
    mutable std::future<bool> value_;
public:
    template<typename Functor>
    lazy(Functor &&f)
    : value_{ std::async(std::launch::deferred,
                         std::forward<Functor>(f)) }
    {
    }

    operator bool() const
    {
         return value_.get();
    }
};

код клиента:

auto b1 = lazy::lazy{[&]{ return func1(param1,param2,param3); }};
auto b2 = lazy::lazy{[&]{ return func2(param4); }};
// ...
bool const b15 = lazy::lazy{[&]{ return func15(param35,param36,param37,param38); }};

// rest remains the same as your contrieved example

Я не компилировал этот код. Если вы работаете в С ++ 14 (как вы упомянули), вам может потребоваться заводская функция, подобная этой:

template<typename Functor>
auto make_lazy(Functor&& f) { return lazy<Functor>(std::forward<Functor>(f)); }

Единственное, что изменится, - это объявление ваших переменных bX. Вы также можете рассмотреть возможность добавления кода, который сообщает вам, как часто на практике вызывается каждое ленивое вычисление, сначала объявляя эти переменные bX и запуская их немедленно, параллельно, а не в отложенном режиме. Но делайте это только после того, как вы измеряете производительность обоими способами.

person utnapistim    schedule 16.09.2020