我似乎无法理解为什么下面的类型为const int的代码编译:
int main() { using T = int; const T x = 1; auto lam = [] (T p) { return x+p; }; } $ clang++ -c lambda1.cpp -std=c++11 $
而这个类型为const double的那个不是:
int main() { using T = double; const T x = 1.0; auto lam = [] (T p) { return x+p; }; } $ clang++ -c lambda2.cpp -std=c++11 lambda1.cpp:5:32: error: variable 'x' cannot be implicitly captured in a lambda with no capture-default specified auto lam = [] (T p) { return x+p; }; ^ lambda1.cpp:4:11: note: 'x' declared here const T x = 1.0; ^ lambda1.cpp:5:14: note: lambda expression begins here auto lam = [] (T p) { return x+p; }; ^ 1 error generated.
但是用constexpr double编译:
int main() { using T = double; constexpr T x = 1.0; auto lam = [] (T p) { return x+p; }; } $ clang++ -c lambda3.cpp -std=c++11 $
为什么int的行为不同于double,或者除了int之外的任何其他类型,即int被const限定符接受,但double/other类型必须是constexpr?另外,为什么这段代码用C++ 11编译,我从[1]的理解是这种隐式捕获是C++ 14的特性.
.. [1] 这个带有空捕获列表的lambda如何能够引用到达范围名称?
这样做的原因是为了保持C++ 03兼容性,因为在C++ 03中使用常量表达式初始化的const整数或const枚举类型可用于常量表达式,但浮点数不是这种情况.
保留限制的基本原理可以在C++ 11之后的缺陷报告1826中找到(这解释了ABI中断评论)并询问(强调我的):
用常量初始化的const整数可以用在常量表达式中,但是用常量初始化的const浮点变量不能.这是故意的,与C++ 03兼容,同时鼓励constexpr的一致使用.然而,有些人发现这种区别令人惊讶.
还观察到允许const浮点变量作为常量表达式将是ABI突破性变化,因为它将影响λ捕获.
一种可能性是在常量表达式中不推荐使用const积分变量.
而回应是:
CWG认为当前的规则不应该改变,并且希望浮点值参与常量表达式的程序员应该使用constexpr而不是const.
我们可以注意到这个问题指出允许const浮点变量是常量表达式将是关于lambda捕获的ABI中断.
这是因为lambda不需要捕获变量,如果它没有使用odr并且允许const浮点变量是常量表达式将允许它们落在这个异常之下.
这是因为在常量表达式中允许使用常量表达式或constexpr文字类型初始化的const整数或枚举类型的左值到右值转换.对于使用常量表达式初始化的const浮点类型,不存在此类异常.这在C++ 11标准部分[expr.const] p2草案中有所介绍:
条件表达式是核心常量表达式,除非它涉及以下之一作为潜在评估的子表达式[...]
并包含在[expr.const] p2.9中
除非适用,否则左值到右值的转换(4.1)
一个整数或枚举类型的glvalue,它引用一个带有前面初始化的非易失性const对象,用一个常量表达式初始化,或者
一个文字类型的glvalue,它指的是用constexpr定义的非易失性对象,或者指的是这样一个对象的子对象,或者
如果不再需要捕获非odr使用的const浮点值(这是ABI中断),则更改此值可能会影响lambda对象的大小.这个限制最初是为了保持C++ 03兼容性并鼓励使用constexpr,但现在这个限制已经到位,很难将其删除.
注意,在C++ 03中,我们只允许为const integral或const枚举类型指定一个类的常量初始化器.在C++ 11中,这被扩展了,我们被允许使用大括号或等号初始化器为constexpr文字类型指定常量初始化器.
根据标准§5.1.2/ p12 Lambda表达式[expr.prim.lambda](Emphasis Mine):
具有相关捕获默认值的lambda表达式(未明确捕获)
this
或具有自动存储持续时间的变量(这排除了任何已发现引用initcapture关联的非静态数据成员的id表达式),被认为是隐含的this
如果复合语句捕获实体(即,变量):(12.1) - 使用(3.2)实体,或
(12.2) - 在一个可能被评估的表达式(3.2)中命名实体,其中封闭的full-expression依赖于在lambda-expression的到达范围内声明的泛型lambda参数[示例:
void f(int, const int (&)[2] = {}) { } // #1 void f(const int&, const int (&)[1]) { } // #2 void test() { const int x = 17; auto g = [](auto a) { f(x); // OK: calls #1, does not capture x }; auto g2 = [=](auto a) { int selector[sizeof(a) == 1 ? 1 : 2]{}; f(x, selector); // OK: is a dependent expression, so captures x }; }- 结束示例]所有这些隐式捕获的实体都应在lambda表达式的到达范围内声明.[注意:嵌套的lambda表达式对实体的隐式捕获可以通过包含lambda表达式来隐式捕获它(见下文).隐含的odr用法可能导致隐式捕获. - 结束说明]
这里的标准是,如果使用了odr,则需要捕获lambda中的变量.通过使用odr,标准意味着需要变量定义,因为它的地址被采用或者有对它的引用.
然而,这条规则有例外.其中一个特别感兴趣的是标准§3.2/ p3一个定义规则[basic.def.odr](Emphasis Mine):
变量x的名称显示为潜在评估的表达式ex,除非将lvalue-to-rvalue转换(4.1)应用于x,否则将生成一个不调用任何非平凡函数的常量表达式(5.20) x是一个对象,ex是表达式e的潜在结果集的一个元素,...
现在如果在例子中:
int main() { using T = int; const T x = 1; auto lam = [] (T p) { return x+p; }; }
和
int main() { using T = double; constexpr T x = 1.0; auto lam = [] (T p) { return x+p; }; }
将左值应用于右值转换,x
我们得到一个常量表达式,因为在第一个例子中x
是一个整数常量,在第二个例子中x
是声明的constexpr
.因此,x
不需要在这些上下文中捕获.
但是,这不是示例的情况:
int main() { using T = double; const T x = 1.0; auto lam = [] (T p) { return x+p; }; }
在这个例子中,如果我们将左值应用于右值转换,x
我们就不会得到常量表达式.
现在,你可能想知道为什么是这种情况,因为x
是const double
.那么答案是,如果一个变量constexpr
是一个常量整数或一个枚举类型,那么声明的变量没有限定为常量表达式,并且在声明时用一个常量表达式初始化.这可以通过§5.20/ p2.7.1常量表达式[expr.const](Emphasis Mine)中的标准来证明:
条件表达式e是核心常量表达式,除非根据抽象机器(1.9)的规则评估e将评估以下表达式之一:
...
(2.7) - 左值 - 右值转换(4.1),除非适用于
(2.7.1) - 一个非整数或枚举类型的非易失性glvalue ,它引用一个完整的非易失性const对象,具有前面的初始化,用常量表达式初始化,......
因此,const double
需要捕获变量,因为左值到右值的转换不会使常量表达式大喊大叫.因此,你理所当然地得到了编译错误.