为什么 lambda 没有从到达范围捕获类型 const double,而 const int 是?
Why type const double is not captured by lambda from reaching-scope, but const int is?
我似乎无法理解为什么以下类型为 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]
根据标准 §5.1.2/p12 Lambda 表达式[expr.prim.lambda](Emphasis Mine):
A lambda-expression with an associated capture-default that does not
explicitly capture this
or a variable with automatic storage duration
(this excludes any id-expression that has been found to refer to an
initcapture’s associated non-static data member), is said to
implicitly capture the entity (i.e., this
or a variable) if the
compound-statement:
(12.1) - odr-uses (3.2) the entity, or
(12.2) - names the entity in a potentially-evaluated expression (3.2) where the
enclosing full-expression depends on a generic lambda parameter
declared within the reaching scope of the lambda-expression
[Example:
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
};
}
— end example ] All such implicitly captured entities shall be
declared within the reaching scope of the lambda expression. [ Note:
The implicit capture of an entity by a nested lambda-expression can
cause its implicit capture by the containing lambda-expression (see
below). Implicit odr-uses of this can result in implicit capture. —
end note ]
此处标准规定的是,如果 lambda 中的变量被 odr 使用,则需要捕获它。通过 odr-used 标准意味着需要变量定义,因为它的地址已被占用或存在对它的引用。
但是这条规则也有例外。其中一个特别有趣的是在标准 §3.2/p3 中找到一个定义规则 [basic.def.odr](Emphasis Mine):
A variable x whose name appears as a potentially-evaluated expression
ex is odr-used by ex unless applying the lvalue-to-rvalue conversion
(4.1) to x yields a constant expression (5.20) that does not invoke
any nontrivial functions and, if x is an object, ex is an element of
the set of potential results of an expression 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)中的标准证明了这一点:
A conditional-expression e is a core constant expression unless the
evaluation of e, following the rules of the abstract machine (1.9),
would evaluate one of the following expressions:
...
(2.7) - an lvalue-to-rvalue conversion (4.1) unless it is applied to
(2.7.1) - a non-volatile glvalue of integral or enumeration type that
refers to a complete non-volatile const object with a preceding
initialization, initialized with a constant expression, ...
因此,const double
需要捕获变量,因为从左值到右值的转换不会发出常量表达式。因此,您理所当然地会遇到编译器错误。
这样做的最终原因是为了保持 C++03 的兼容性,因为在 C++03 中,用常量表达式初始化的 const 整数或 const 枚举类型可用于常量表达式,但浮动不是这种情况点.
保留限制的基本原理可以在 C++11 之后的 defect report 1826 中找到(这解释了 ABI 中断注释)并询问(强调我的):
A const integer initialized with a constant can be used in constant expressions, but a const floating point variable initialized with a constant cannot. This was intentional, to be compatible with C++03 while encouraging the consistent use of constexpr. Some people have found this distinction to be surprising, however.
It was also observed that allowing const floating point variables as constant expressions would be an ABI-breaking change, since it would affect lambda capture.
One possibility might be to deprecate the use of const integral variables in constant expressions.
响应是:
CWG felt that the current rules should not be changed and that programmers desiring floating point values to participate in constant expressions should use constexpr instead of const.
我们可以注意到问题指出,允许 const 浮点数 变量是常量表达式将是关于 lambda 捕获的 ABI 中断。
之所以如此,是因为 lambda 不需要捕获未使用 odr 的变量,并且允许 const 浮点变量为常量表达式将使它们属于此异常。
这是因为在常量表达式中允许使用常量表达式或 constexpr 文字类型初始化的 const 整数或枚举类型的左值到右值转换。对于使用常量表达式初始化的 const 浮点类型,不存在此类异常。这包含在草案 C++11 标准部分 [expr.const]p2:
A conditional-expression is a core constant expression unless it involves one of the following as a potentially
evaluated subexpression [...]
并包含在 [expr.const]p2.9
中
- an lvalue-to-rvalue conversion (4.1) unless it is applied to
- a glvalue of integral or enumeration type that refers to a non-volatile const object with a preceding initialization, initialized
with a constant expression, or
- a glvalue of literal type that refers to a non-volatile object defined with constexpr, or that refers to a sub-object of such an
object, or
更改此设置可能会影响 lambda 对象的大小,如果它们不再需要捕获非 odr 使用的 const 浮点值(这是 ABI 中断)。此限制最初是为了保持 C++03 兼容性并鼓励使用 constexpr 但现在此限制已到位,很难删除它。
注意,在 C++03 中 we were only allowed to specify an in class constant-initializer for const integral or const enumeration types。在 C++11 中,这得到了扩展,我们可以使用大括号或等于初始化器为 constexpr 文字类型指定常量初始化器。
我似乎无法理解为什么以下类型为 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]
根据标准 §5.1.2/p12 Lambda 表达式[expr.prim.lambda](Emphasis Mine):
A lambda-expression with an associated capture-default that does not explicitly capture
this
or a variable with automatic storage duration (this excludes any id-expression that has been found to refer to an initcapture’s associated non-static data member), is said to implicitly capture the entity (i.e.,this
or a variable) if the compound-statement:(12.1) - odr-uses (3.2) the entity, or
(12.2) - names the entity in a potentially-evaluated expression (3.2) where the enclosing full-expression depends on a generic lambda parameter declared within the reaching scope of the lambda-expression [Example:
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 }; }
— end example ] All such implicitly captured entities shall be declared within the reaching scope of the lambda expression. [ Note: The implicit capture of an entity by a nested lambda-expression can cause its implicit capture by the containing lambda-expression (see below). Implicit odr-uses of this can result in implicit capture. — end note ]
此处标准规定的是,如果 lambda 中的变量被 odr 使用,则需要捕获它。通过 odr-used 标准意味着需要变量定义,因为它的地址已被占用或存在对它的引用。
但是这条规则也有例外。其中一个特别有趣的是在标准 §3.2/p3 中找到一个定义规则 [basic.def.odr](Emphasis Mine):
A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion (4.1) to x yields a constant expression (5.20) that does not invoke any nontrivial functions and, if x is an object, ex is an element of the set of potential results of an expression 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)中的标准证明了这一点:
A conditional-expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine (1.9), would evaluate one of the following expressions:
...
(2.7) - an lvalue-to-rvalue conversion (4.1) unless it is applied to
(2.7.1) - a non-volatile glvalue of integral or enumeration type that refers to a complete non-volatile const object with a preceding initialization, initialized with a constant expression, ...
因此,const double
需要捕获变量,因为从左值到右值的转换不会发出常量表达式。因此,您理所当然地会遇到编译器错误。
这样做的最终原因是为了保持 C++03 的兼容性,因为在 C++03 中,用常量表达式初始化的 const 整数或 const 枚举类型可用于常量表达式,但浮动不是这种情况点.
保留限制的基本原理可以在 C++11 之后的 defect report 1826 中找到(这解释了 ABI 中断注释)并询问(强调我的):
A const integer initialized with a constant can be used in constant expressions, but a const floating point variable initialized with a constant cannot. This was intentional, to be compatible with C++03 while encouraging the consistent use of constexpr. Some people have found this distinction to be surprising, however.
It was also observed that allowing const floating point variables as constant expressions would be an ABI-breaking change, since it would affect lambda capture.
One possibility might be to deprecate the use of const integral variables in constant expressions.
响应是:
CWG felt that the current rules should not be changed and that programmers desiring floating point values to participate in constant expressions should use constexpr instead of const.
我们可以注意到问题指出,允许 const 浮点数 变量是常量表达式将是关于 lambda 捕获的 ABI 中断。
之所以如此,是因为 lambda 不需要捕获未使用 odr 的变量,并且允许 const 浮点变量为常量表达式将使它们属于此异常。
这是因为在常量表达式中允许使用常量表达式或 constexpr 文字类型初始化的 const 整数或枚举类型的左值到右值转换。对于使用常量表达式初始化的 const 浮点类型,不存在此类异常。这包含在草案 C++11 标准部分 [expr.const]p2:
A conditional-expression is a core constant expression unless it involves one of the following as a potentially evaluated subexpression [...]
并包含在 [expr.const]p2.9
中
- an lvalue-to-rvalue conversion (4.1) unless it is applied to
- a glvalue of integral or enumeration type that refers to a non-volatile const object with a preceding initialization, initialized with a constant expression, or
- a glvalue of literal type that refers to a non-volatile object defined with constexpr, or that refers to a sub-object of such an object, or
更改此设置可能会影响 lambda 对象的大小,如果它们不再需要捕获非 odr 使用的 const 浮点值(这是 ABI 中断)。此限制最初是为了保持 C++03 兼容性并鼓励使用 constexpr 但现在此限制已到位,很难删除它。
注意,在 C++03 中 we were only allowed to specify an in class constant-initializer for const integral or const enumeration types。在 C++11 中,这得到了扩展,我们可以使用大括号或等于初始化器为 constexpr 文字类型指定常量初始化器。