Reputation: 13
I'm trying to figure out the best way to simplify conditional statements that are of the form:
if( ( A() && B() ) || ( B() && C() ) || ( C() && D() ) || .... )
{
code
}
The A/B/C/D/etc functions are relatively expensive, so it's not ideal that they could each be called twice.
Two alternatives I've thought of:
bool a = A();
bool b = B();
bool c = C();
bool d = D();
if( ( a && b ) || ( b && c ) || ( c && d ) || .... )
{
code
}
This version is not ideal because C(), D(), and any additional conditions are evaluated every time even if A() and B() were both true. In the original version, they wouldn't have been called at all in that case.
bool b = B();
if( ( A() && b ) )
{
code
}
else
{
bool c = C();
if( ( b && c ) )
{
code
}
else
{
bool d = D();
if( ( c && D ) )
{
code
}
else
{
...
}
}
}
This version avoids any duplicate and unnecessary conditions from being evaluated, but is incredibly verbose and painful to write.
So I'm hopeful that there's some simpler but equally effective way to write it that I'm not thinking of...?
Upvotes: 1
Views: 885
Reputation: 25327
This is very similar to an adjacent_find
, but using adjacent_find
would require reevaluating A
, B
, etc sometimes. We can write our own version of it that handles this by customizing on a predicate, rather than on "comparing equal":
template <typename FwdIter, typename Pred>
FwdIter find_if_consecutive(FwdIter cur, FwdIter last, Pred pred) {
if (cur == last) return last;
bool curMatches = false, nextMatches = pred(*cur);
for (auto next = std::next(cur); next != last; ++cur, ++next) {
curMatches = std::exchange(nextMatches, pred(*next));
if (curMatches && nextMatches) return cur;
// Note: this *might* possibly be faster by moving
// forward by 2 when `nextMatches` is false, which
// avoids one of the `curMatches && nextMatches`.
// Implementation left as an exercise for the reader.
}
return last;
}
This can then be used like so, if A
, B
, C
, ... can all be function pointers of the same type:
auto fns = { A, B, C, D }; // Create an initializer_list
return fns.end()
!= find_if_consecutive(fns.begin(), fns.end(), [](auto f) { return f(); });
If we can't put the different expressions into a homogeneous type, we'd need a heterogeneous algorithms library; perhaps Boost Hana would work.
Upvotes: 2
Reputation: 35154
You could use assignment expressions within the if
.
bool b,c;
if (((b = B()) && A()) || ((c = C()) && b) || (c && D()) ) {
cout << "done";
}
As pointed out by @abdullah, one has to take care that due to shortcut evaluation, variable b
might not have been initialized when used in the condition after the ||
. So the expressions which's result shall be reused later must be on the left hand side of an &&
-operator, which might introduce unnecessary evaluations.
A way to avoid this would be to use a tristate logic where the variable "knows" if it has been assigned yet. C++ does not have tristate booleans, but it can be simulated easily through data type int
:
int a = A();
int b=-1;
int c=-1;
int d=-1;
if(
( a && (b=B()) ) || ( (b<0?b=B():b) && (c=C()) ) || ( (c<0?c=C():c) && (d=D()) )
)
{
cout << "done";
}
Upvotes: 1
Reputation: 20396
One version involves saving all the functions into a std::vector<std::function<bool()>>
, which creates very simple calling-conventions and simplifies the logic:
#include<vector>
#include<functional>
#include<iostream>
bool evaluate_vec(std::vector<std::function<bool()>> const& funcs) {
bool a, b;
for (size_t index = 0; index < funcs.size(); index++) {
//We'll ping-pong between the two cached evaluations of the variables
if ((index & 1) == 0)
a = funcs[index]();
else
b = funcs[index]();
if (index > 0)
//The short-circuiting behavior we intend to have
if (a && b)
return true;
}
return false;
}
int main() {
bool evaluation = evaluate_vec({ A, B, C, D });
}
Upvotes: 0
Reputation: 20396
Solve the problem with Template Meta-Programming!
//If we run out of functions and didn't find a pair that both returned true, we return false
bool evaluate_impl(bool) {
return false;
}
//At this point, we're guaranteed to have at least 1 cached result and 1 unevaluated function call.
template<typename Func, typename ... Funcs>
bool evaluate_impl(bool cached, Func && func, Funcs&& ... funcs) {
//store the result of the function
bool result = func();
if (cached && result)
return true;
else {
//the result of this call becomes the new cached value
return evaluate_impl(result, std::forward<Funcs>(funcs)...);
}
}
//We receive all the functions
template<typename Func, typename ... Funcs>
bool evaluate_tmp(Func && func, Funcs&& ... funcs) {
//We cache the result of calling the first function and pass along to the next step
return evaluate_impl(func(), std::forward<Funcs>(funcs)...);
}
int main() {
bool result = evaluate_tmp(A, B, C, D);
}
This efficiently expands to support any number of function calls (up to what your compiler will allow, at any rate).
int main() {
bool result = evaluate_tmp(A, B, A, C, A, A, A, A, D, A, B, A, C, A, D, A);
}
Upvotes: 0
Reputation: 662
bool a = A(), b, c, d;
if (((b = B()) && a) || ((c = C()) && b) || (d = D() && c) || ...) {
// code;
}
Here if a
is false, B()
is evaluated for next condition check. In the second condition, if b
is false, C()
is evaluated for its next condition. In this way, we can make sure every function is evaluated when needed. But for the last condition, we should use function evaluation as the second operand.
Upvotes: 3