【问题标题】:How to make `short-circuit evaluation` also available in `fold expressions`?如何在“折叠表达式”中也可以使用“短路评估”?
【发布时间】:2017-08-20 05:54:44
【问题描述】:
#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!
}

传统的&amp;&amp;操作符支持短路求值,所以false &amp;&amp; (*pb = true)在运行时是可以的,但是下面两种情况不行。

如何在fold expressions 中也提供短路评估

【问题讨论】:

  • 这里的问题根本不是折叠表达式。试试constexpr bool AND(bool a, bool b) { return a &amp;&amp; b; }。问题是在调用函数之前必须评估所有参数,并且传入的是它们的 result
  • 你永远不会新pb,所有*pb = true 都是未定义的行为。
  • 运行时评估的原因是因为*pb = true 本身不是constexpr
  • @appleapple 这就是重点:在取消引用之前使用短路检查nullptr
  • @Quentin 谢谢,这对我来说不是那么明显。 OP似乎因此而感到困惑。如果 OP 尝试使用其他不依赖于未定义行为的表达式。这样做可能会更清楚。

标签: c++ standards c++17 short-circuiting fold-expression


【解决方案1】:

这里的问题只是对实际发生的事情的误解。

如何使短路评估也可用于折叠表达式?

在折叠表达式中可用。 (args &amp;&amp; ... ) 遵循与(a &amp;&amp; b &amp;&amp; c &amp;&amp; d) 完全相同的规则。也就是说,d 将仅在 abc 都评估为真时评估。

这不是你的两个案例之间的实际区别。

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

虽然折叠表达式的作用与其非折叠表达式完全相同,但这两个语句之间有一个重要区别。第一个只是一个语句表达式,第二个是函数调用。并且必须在主体开始之前评估所有函数参数。

所以第二个等价于:

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

导致问题的是排序,而不是折叠表达式(注意:b 可以在a 之前进行评估)。

为了使其透明化,您真正需要的是惰性参数。这是几种语言中的一个特性(例如Scala),但在 C++ 中没有。如果你需要懒惰,你能做的最好的就是把所有东西都包装在一个 lambda 中:

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

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

然后你可以让这个任意复杂 - 可能只“解包”那些可调用的类型,否则假设它们是 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; });

但实际上,主要的一点是函数参数评估在函数体之前,问题不在于折叠表达式本身。

【讨论】:

  • 函数参数求值在函数体中的语句求值之前进行排序。这是 applicative 评估,一种顺序形式的 strict 评估,它保证每个参数都被评估,尽管主体。请注意,参数本身不是惰性的。函数调用可以使用名称调用策略来实现非严格求值。 OTOH,惰性求值 (call-by-need),是 call-by-name 策略的记忆形式,在这里不一定需要(尽管如果所有内容都求值一次,则没有区别)。
  • 有趣的是,使用包装好的 thunk 并不是区分严格和非严格形式的唯一方法。这里的 thunk 就像 Lisp 的 quote'd 形式,它在额外的间接调用中传达了非严格性。比较直接的方法是:将应用评估视为 操作 的包装形式,这看起来更简洁,因为对参数的评估+对主体的评估更多 比在整体操作语义上评估身体。遗憾的是,大多数语言都不会受到这种青睐。
  • 其实我在这里找到问题的直接原因是我找不到map/reduce/fold/accumulate-like算法中性二元函数的严格和非严格形式。大多数都是严格的,少数(例如在 Haskell 中)完全依赖于懒惰。即使是Kernel,这是最早明确明确评估风格(针对quote)的语言之一,也需要its reduce only accept an applicative binary。仍在寻找更通用的...
猜你喜欢
  • 2021-05-13
  • 1970-01-01
  • 2013-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多