Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make `short-circuit evaluation` also available in `fold expressions`?

#include <type_traits>

#define FORWARD(arg)\
std::forward<decltype(arg)>(arg)

template<typename... Args>
constexpr bool AndL(Args&&... args)
{
    return (... && FORWARD(args));
}

template<typename... Args>
constexpr bool AndR(Args&&... args)
{
    return (FORWARD(args) && ...);
}

int main()
{
    bool* pb = nullptr;

    false && (*pb = true);       // ok at runtime.
    AndL(false, (*pb = true));  // error at runtime!
    AndR(false, (*pb = true));  // error at runtime!
}

The traditional && operator supports short-circuit evaluation, so false && (*pb = true) will be ok at runtime, but the following two cases are not.

How to make short-circuit evaluation also available in fold expressions?

like image 301
xmllmx Avatar asked Mar 27 '17 13:03

xmllmx


1 Answers

The problem here is just a misconception of what's actually happening.

How to make short-circuit evaluation also available in fold expressions?

It is available in fold expressions. (args && ... ) follows the exactly the same rules as (a && b && c && d). That is, d will only be evaluated if a, b, and c all evaluate to truthy.

That's not the actual difference between your two cases.

false && (*pb = true);       // ok at runtime.
AndL(false, (*pb = true));   // error at runtime!

While fold expressions do exactly the same thing as their non-fold counterparts, there's one important difference between these two statements. The first is just a statement-expression, the second is a function call. And all function arguments must be evaluated before the start of the body begins.

So the second is equivalent to:

auto&& a = false;
auto&& b = (*pb = true);
(FORWARD(a) && FORWARD(b));

It's that ordering that is causing the problem, not the fold expression (note: b could be evaluated before a).

In order to make this transparent, what you really need are lazy arguments. This is a feature in several languages (e.g. Scala), but not in C++. If you need laziness, the best you could do is wrap everything in a lambda:

template<typename... Args>
constexpr bool AndL(Args&&... args)
{
    return (... && FORWARD(args)());
}

AndL([]{ return false; }, [&]{ return *pb = true; });

You could then make this arbitrarily complex - maybe only "unwrap" those types that are callable, otherwise assume that they're bool:

template <class T, std::enable_if_t<std::is_invocable<T>::value, int> = 0>
bool unwrap(T&& val) { return std::forward<T>(val)(); }

template <class T, std::enable_if_t<std::is_convertible<T, bool>::value, int> = 0>
bool unwrap(T&& val) { return std::forward<T>(val); }

template<typename... Args>
constexpr bool AndL(Args&&... args)
{
    return (... && unwrap(FORWARD(args)));
}

AndL(false, [&]{ return *pb = true; });

But really, the main point is that function argument evaluation precedes the function body, and the issue is not the fold expression itself.

like image 163
Barry Avatar answered Nov 07 '22 19:11

Barry