How to defer computation in C++ until needed?

826 Views Asked by At

In C++(*), is it possible to have a structure that "defers" some computation until needed (and maybe never does the computation if not necessary)? My use case is as follows: I have roughly a dozen bool variables, each of which is computed with some function call. Following that, there is a rather long (and complex) conditional statement that uses those bool variables in different combinations to determine what action the code will take next.

Here is some contrived sample code to hopefully better illustrate what I'm doing:

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

That is a purely contrived example, but hopefully it illustrates the idea.

Clearly this code could be re-written without the bools, and the functions called directly in the big conditional statement. But I feel that would make the already not-easy-to-read code even harder to read, and more error prone. And this logic could change, so I feel the bools make it easier to manage from a refactoring perspective as well.

Furthermore, any bool might be referenced multiple times within the conditional; so using the functions directly means execution could be duplicated. (I was thinking std::bind might get me there from a readability perspective; but it would still potentially call any of the funcN() calls multiple times.)

What I'm looking for is the best of both words, like a "deferred" compute. What if instead of being computed and assigned explicitly at the start of the code, I could say, "only evaluate these as needed (and remember the result)". The big conditional statement is such that, generally, not all bools actually need to be computed to determine what happens next. The goal here is improved performance, as this code is called often. So I'm trying to reduce the amount of work done on each iteration.

(*) Preferably C++14 (or older), as that's what my employer is using.

Edit: What about something like this:

#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;
}

Output:

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
4

There are 4 best solutions below

1
prog-fh On

At first, I would try using some lambda-closures.

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(); }
...

If you need to cache the bool results but not for the entire lifetime of the program (static), this solution could make it (three levels of lambda-closure; it's "Inception").

/**
  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
**/
2
UKMonkey On

std::async with the option std::launch::deferred is what you're looking for.

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.
0
Den-Jason On

For the sake of readability and maintainability you could organise the program as a state machine. That provides you with the benefit of separating the state transitions and actions from one another, plus it should be reasonably simple to rewire the logic later should the necessity arise.

See here for some examples: C++ code for state machine

0
utnapistim On

What if instead of being computed and assigned explicitly at the start of the code, I could say, "only evaluate these as needed (and remember the result)"

/// @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();
    }
};

client code:

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

I have not compiled this code. If working in c++14 (as you mention) you may need a factory function similar to this:

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

The only thing that changes is the declaration of your bX variables. You may also consider adding code that tells you how often each lazy evaluation is called in practice, declaring those bX variables first, and launching them immediately, in parallel, instead of in a deferred manner. But only do that after you measure performance both ways.