为什么lambda不会从达到范围捕获类型const double,但const int是?

时间:2015-12-16 22:38:13

标签: c++ c++11 lambda 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] how is this lambda with an empty capture list able to refer to reaching-scope name?

2 个答案:

答案 0 :(得分:14)

这样做的原因是为了保持C ++ 03兼容性,因为在C ++ 03中使用常量表达式初始化的const整数或const枚举类型可用于常量表达式,但浮点数不是这种情况。

保持限制的基本原理可以在C ++ 11之后的defect report 1826中找到(这解释了ABI中断评论)并询问(强调我的) EM>):

  

用常量初始化的const整数可以用在常量表达式中,但是用常量初始化的const浮点变量不能。这个是故意的,与C ++ 03兼容,同时鼓励一致使用constexpr 。然而,有些人发现这种区别是令人惊讶的。

     

还观察到允许const浮点变量作为常量表达式将是ABI突破性变化,因为它会影响lambda捕获。

     

一种可能性是在常量表达式中不推荐使用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 we were only allowed to specify an in class constant-initializer for const integral or const enumeration types中。在C ++ 11中,这被扩展了,我们被允许使用大括号或等号初始化器为constexpr文字类型指定常量初始化器。

答案 1 :(得分:10)

根据标准§5.1.2/ p12 Lambda表达式[expr.prim.lambda]( Emphasis Mine ):

  

具有相关捕获默认值的lambda表达式   明确捕获this或具有自动存储持续时间的变量   (这排除了任何被发现引用的id表达式   据说是initcapture的相关非静态数据成员   隐含地捕获实体(即this或变量)   复合声明:

     

(12.1) - odr-uses(3.2)实体,或

     

(12.2) - 在一个可能被评估的表达式(3.2)中命名实体   封闭full-expression取决于通用lambda参数   在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-expression导致其隐式捕获(参见   下面)。隐含的odr用法可能导致隐式捕获。 -   结束说明]

这里的标准是,如果使用了odr,则需要捕获lambda中的变量。通过使用odr,标准意味着需要变量定义,因为它的地址被采用或者是对它的引用。

然而,这条规则有例外。其中一个特别感兴趣的是标准§3.2/ p3一个定义规则[basic.def.odr]( Emphasis Mine ):

  

变量x,其名称显示为可能已评估的表达式   ex除非应用左值到右值的转换,否则由ex使用   (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,我们就不会得到常量表达式。

现在您可能想知道为什么会这样,因为xconst double。那么答案是,如果一个是常量整数或枚举类型,那么声明没有constexpr的变量就可以作为常量表达式,并且在声明时用一个常量表达式初始化。 §5.20/ p2.7.1常量表达式[expr.const]( Emphasis Mine )中的标准证明了这一点:

  

条件表达式e是核心常量表达式,除非   评估e,遵循抽象机的规则(1.9),   将评估以下表达式之一:

     

...

     

(2.7) - 左值 - 右值转换(4.1),除非它适用于

     

(2.7.1) - 整数或枚举类型的非易失性glvalue   是指具有前面的完整的非易失性const对象   初始化,用常量表达式初始化,...

因此,需要捕获const double变量,因为左值到右值的转换不会使常量表达式大喊。因此,理所当然地会出现编译错误。