简体   繁体   中英

Simplify overlapping And condition statements

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...?

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.

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

Live on Wandbox


If we can't put the different expressions into a homogeneous type, we'd need a heterogeneous algorithms library; perhaps Boost Hana would work.

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

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM