GCC 和 Clang 不同意 C++17 constexpr lambda 捕获

GCC and Clang disagree about C++17 constexpr lambda captures

考虑这个示例,它声明一个变量为 constexpr,通过 lambda 中的副本捕获它,并声明另一个 constexpr 变量,该变量是 constexpr 函数从原始变量展开非类型模板参数的结果。

#include <utility>

template<int I>
constexpr auto unwrap(std::integral_constant<int, I>) {
  return I;
}

int main() {
  constexpr auto i = std::integral_constant<int, 42>{};
  constexpr auto l = [i]() {
    constexpr int x = unwrap(i);
  };
}

Clang(中继线)接受此代码。 (wandbox)

GCC(主干)失败并显示以下错误消息 (wandbox):

lambda_capture.cpp:11:31: error: the value of ‘i’ is not usable in a constant expression
     constexpr int x = unwrap(i);
                               ^
lambda_capture.cpp:10:28: note: ‘i’ was not declared ‘constexpr’
   constexpr auto l = [i]() {

哪个编译器是正确的?在我看来,这是一个 GCC 错误,其中 lambda 捕获的 constexpr-ness 没有正确传播到 lambda 上下文。

这两种实现都有漏洞,但我倾向于认为 GCC 在这里得到了正确的答案。


删除 i 的捕获会导致 Clang 拒绝编译代码。这意味着它显然在某处存在错误。

[expr.const]/2.12:

An expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine, would evaluate one of the following expressions:

  • [...]
  • in a lambda-expression, a reference to [...] a variable with automatic storage duration defined outside that lambda-expression, where the reference would be an odr-use;
  • [...]

Clang 的行为是精神分裂的:如果在正文中使用 i 不是 odr-use,那么它不需要被捕获,但它拒绝 OP 中的代码,如果显式捕获被删除; OTOH,如果它是 odr-use,那么上面的 unwrap(i) 不是常量表达式,因此它应该拒绝 x.

的初始化

GCC 的 lambda 实现在 odr-use 方面非常糟糕。它会超早地不断折叠,导致各种微妙的恶作剧。另一方面,对于显式捕获,它会转换所有用途,无论它是否实际上是 odr-use。激进的常量折叠意味着如果删除 i 的捕获,它会接受 OP 的代码。

假设 unwrap(i) 确实使用了 odr-use i,那么根据 [expr.const]/2.12,OP 的代码格式错误是正确的。


unwrap(i) 实际上是 odr-use i 吗?这个问题 boils down to whether copy-initializing the parameter object of unwrap counts as applying an lvalue-to-rvalue conversion to i. I don't see anything in the standard that explicitly says that an lvalue-to-rvalue conversion is applied here, and instead [dcl.init]/17.6.2 表明我们调用一个构造函数(在这种情况下,是简单的隐式定义的复制构造函数)传递 i 作为参数绑定到它的参数,引用绑定是 odr-use 的经典示例.

可以肯定的是,应用 l-to-r 转换会导致从 i 复制初始化 integral_constant<int, 42> 对象,但这里的问题是标准中没有说明相反 - 来自 iintegral_constant<int, 42> 对象的所有复制初始化都算作 l-to-r 转换。