我编写了以下具有两个 lambda 的代码。其中一个明确捕获了
i
,而另一个则没有。注意 i
是 constexpr,因此我们不需要显式捕获它。
我的问题是为什么
func(lambda2)
不能编译而 func(lambda)
可以编译?请注意,lambda
不会显式捕获 i
,而 lambda2
在其捕获列表中具有 i
。 演示
template <typename T>
constexpr auto func(T c) {
constexpr auto k = c;
return k;
};
int main() {
constexpr int i = 0;
constexpr auto lambda = []()constexpr { constexpr int j = i; return j; }; //compiles
constexpr auto lambda2 = [i]()constexpr { constexpr int j = i; return j; };//compiles
constexpr auto a = func(lambda); //compiles as expected
constexpr auto b = func(lambda2); //this doesn't compile why?
}
叮 说:
error: constexpr variable 'k' must be initialized by a constant expression
3 | constexpr auto k = c;
| ^ ~
/home/insights/insights.cpp:18:24: note: in instantiation of function template specialization 'func<(lambda at /home/insights/insights.cpp:13:30)>' requested here
18 | constexpr auto b = func(lambda2); //this doesn't compile why?
| ^
/home/insights/insights.cpp:3:24: note: function parameter 'c' with unknown value cannot be used in a constant expression
3 | constexpr auto k = c;
| ^
/home/insights/insights.cpp:3:24: note: in call to '(lambda at /home/insights/insights.cpp:13:30)(c)'
/home/insights/insights.cpp:2:23: note: declared here
2 | constexpr auto func(T c) {
| ^
这是一个类似但更简单的代码版本,但无法编译:
template <typename T>
constexpr auto func(int a) {
constexpr auto b = a;
return b;
};
int main() {
constexpr int x = 1;
constexpr int y = func(x);
}
这里,因为
x
是constexpr
,所以在x
变量constexpr
的初始化中读取y
的值就可以了。
但是,这还不够好,因为不仅
y
需要有一个常量表达式作为其初始值设定项,b
也需要一个常量表达式,它也被声明为 constexpr
。这就是问题出现的地方。如果 b
没有被声明 constexpr
,那么代码就可以了。
为了使
constexpr auto b = ...;
有效,编译器必须能够在它出现的上下文中将其计算为常量表达式,而不是调用 func
的更大的封闭上下文。在出现b
初始化的上下文中,直到运行时才知道a
的值。 func
可以在常量表达式和非常量表达式中调用。因此,检查 b
是否为常量表达式失败。
在OP的程序中,非常相似。捕获
lambda2
的 i
属于闭包类型,具有 const int
类型的数据成员 ([expr.prim.lambda.capture]/10)。当调用 func
时,语句
constexpr auto k = c;
在其主体中被实例化,
c
是该闭包类型的函数参数。此时,必须将 k
的初始化作为常量表达式在其出现的上下文中进行检查,而不是在封闭常量计算的上下文中进行检查。初始化使用闭包类型的复制构造函数,默认为 ([expr.prim.lambda.closure]/15),从而执行成员复制。因此,它必须读取函数参数 const int
的 c
成员。
提供更多细节:
[dcl.constexpr]/6 指出“在任何
constexpr
变量声明中,初始化的完整表达式应为常量表达式”。让我们将其应用到 k
的声明中。为了使其成为常量表达式,它必须是满足其他一些条件的核心常量表达式 ([expr.const]/14)。 [expr.const]/5 给出了表达式 E 成为核心常量表达式的条件。在这种情况下,E是k
的init-declarator。
当初始化
k
时,将读取 const int
的 c
数据成员,如上所述。这是左值到右值的转换。 [expr.const]/5.9 指出核心常量表达式 E 中不允许进行左值到右值的转换,除非应用于其中一个
第二个要点显然不满足:
const int
的c
成员的生命周期不是在E的评估内开始的(即k
的init-declarator);当调用该函数时它会提前启动。对于第一个项目符号,函数参数不能在常量表达式中使用,因为它永远不能同时是“常量初始化”和“潜在常量”([expr.const]/4)。特别是,标量或引用类型的函数参数不能满足 [expr.const]/2.1,类类型的函数参数不能满足 [expr.const]/3。
(顺便说一句,即使在
consteval
函数中,函数参数的值仍然不是常量表达式,尽管 consteval
函数只能在常量求值期间调用。没有根本原因为什么会这样在 consteval
的情况下不支持,但如果允许的话,就会造成实现困难,并将几乎每个 consteval
函数变成隐式模板。)