【问题标题】:Lifetime of temporary objects during list-initialization列表初始化期间临时对象的生命周期
【发布时间】:2016-12-25 19:20:52
【问题描述】:

我一直认为,临时对象会一直存在到完整表达式的末尾。然而,std::vector 和数组的初始化之间有一个奇怪的区别。

请考虑以下代码:

#include <iostream>
#include <vector>

struct ID{ 
  static int cnt;
  // the number of living object of class ID at the moment of creation:  
  int id;

  ID():id(++cnt){}

  ~ID(){
     cnt--;
  }
};

int ID::cnt=0;

int main(){

  int arr[]{ID().id, ID().id};
  std::vector<int> vec{ID().id, ID().id};

  std::cout<<" Array: "<<arr[0]<<", "<<arr[1]<<"\n";
  std::cout<<" Vector: "<<vec[0]<<", "<<vec[1]<<"\n";
}

这个程序的输出有点(至少对我来说)出乎意料:

 Array: 1, 1
 Vector: 1, 2

这意味着,在std::vector 的整个初始化过程中,临时对象都是活动的,但在数组的情况下,它们会一个接一个地创建和销毁。我希望临时人员能够活到完整的表达式 int arr[]{ID().id, ID().id}; 完成。

该标准提到了一个关于临时对象的生命周期和数组初始化的例外 (12.2)。但是我不明白它的含义,也不知道为什么在这种特殊情况下应用它:

临时对象在两种情况下被销毁 与完整表达式的结尾不同的点。第一个上下文 是调用默认构造函数来初始化 大批。如果构造函数有一个或多个默认参数,则 在默认参数中创建的每个临时对象的销毁是 在构造下一个数组元素(如果有)之前排序。


使用不同编译器的结果概述(MSVS 结果是 NathanOliver 的屈从):

             Array    Vector
clang 3.8    1, 2      1, 2
g++ 6.1      1, 1      1, 2
icpc 16      1, 1      1, 2
MSVS 2015    1, 1      1, 2

正如 ecatmur 所指出的,对于聚合初始化,braced-init-list 的每个元素都是一个完整表达式,因此以下代码

  struct S{
      int a;
      int b;
  } s{ID().id, ID().id};
  std::cout<<" Struct: "<<s.a<<", "<<s.b<<"\n";

应该将Struct 1, 1 打印到控制台。这正是 g++ 编译的程序所做的。但是,clang 似乎有一个错误 - 生成的程序打印 Struct 1, 2


已向 clang 报告了一个错误:https://llvm.org/bugs/show_bug.cgi?id=29080

【问题讨论】:

  • arr 使用聚合初始化,vec 使用构造函数调用。
  • 似乎是 gcc 的一个错误,clang 为 Demo 提供了 1, 2
  • 公平地说,这里的完整表达式是“ID().id”。然而在向量的情况下,最外层的表达式是对初始化列表构造函数的构造函数调用。 auto &amp;&amp;arr2 = decltype(arr){ID().id, ID().id}; 会发生什么?
  • FWIW MSVS 2015 更新 3 给出与 g++ 相同的结果。
  • @JohannesSchaub-litb auto &amp;&amp;arr2 = decltype(arr){ID().id, ID().id}; 如果使用 g++ 编译,则结果为 {1,1}

标签: c++ c++11 g++ clang


【解决方案1】:

这是core issue 1343 "Sequencing of non-class initialization",2016 年 11 月被论文P0570R0 接受为缺陷报告。提议的决议是 C++17 的一部分,但因此不是 C++14 的一部分,因此(除非委员会决定发布 C++14 的更正)这是 C++17 和 C+ 之间的区别点+14。

C++14

根据C++14标准的规则,正确的输出是数组1, 1,向量1, 2;这是因为构造向量(包括从braced-init-list)需要调用构造函数,而构造数组则不需要。

控制这一点的语言在 [intro.execution] 中:

10 - full-expression 是一个不是另一个表达式的子表达式的表达式。 [...] 如果定义语言构造以产生函数的隐式调用,则语言构造的使用被认为是用于此定义目的的表达式。 [...]

这作为一个顶级概述很好,但它留下了一些未回答的问题:

  • 准确地说,哪个语言构造算作产生函数隐式调用的构造;
  • 实际上算作函数的隐式调用;大概对用户定义的构造函数的调用是对函数的调用,但是默认或定义为默认的构造函数呢?

数组是一个聚合,因此根据[dcl.init.aggr]从一个braced-init-list初始化;这表示每个元素直接从列表的对应元素初始化,因此没有隐式函数调用(至少不对应于整体初始化)。在语法级别,在 initializer ([dcl.init]/1) 中使用 braced-init-list 作为 大括号或相等初始化器,完整表达式是包含在大括号内并用逗号分隔的表达式。在每个完整表达式结束时,临时对象的析构函数都需要运行,因为 [class.temporary] 中提到的三个上下文都不是这里的情况。

向量初始化的情况不同,因为您使用的是initializer_list构造函数,所以会发生函数的隐式调用(即initializer_list构造函数);这意味着在整个初始化过程中存在一个隐式的完整表达式,因此只有在向量的初始化完成时才会销毁临时对象。

令人困惑的是,[dcl.init.list] 说您的代码“大致相当于”:

const int __a[2] = {int{ID().id}, int{ID().id}};  // #1
std::vector<int> vec(std::initializer_list<int>(__a, __a + 2));

但是,这必须在上下文中读取 - 例如,支持 initializer_list 的数组的生命周期受向量初始化的限制。

这在 C++03 中要清楚得多,在 [intro.execution] 中有:

13 - [注意: C++ 中的某些上下文会导致对由句法构造产生的 full-expression 求值 除了表达式 (5.18)。例如,在 8.5 中,initializer 的一种语法是 ( expression-list ) 但生成的构造是对构造函数的函数调用,以 expression-list 作为参数 列表;这样的函数调用是一个完整的表达式。例如,在 8.5 中,initializer 的另一种语法是 = initializer-clause 但同样,生成的构造可能是对具有一个 assignment-expression 的构造函数的函数调用 作为论据;同样,函数调用是一个完整的表达式。 ]

这一段是从 C++11 中完整删除的;这是根据CWG 392 的决议。由此产生的混乱可能不是故意的。

C++17

在 P0570R0 之后,[intro.execution] 声明 full-expression 是:[...]

  • 一个 init-declarator ([dcl.decl]) [...] 包括初始化器的组成表达式,或 [...]
  • 一个表达式不是另一个表达式的子表达式,也不是完整表达式的一部分。

所以在 C++17 中,完整表达式分别为 arr[]{ID().id, ID().id}vec{ID().id, ID().id},在每种情况下正确的输出是 1, 2,因为第一个临时 ID 的销毁推迟到完整表达式的结尾。

【讨论】:

  • 刚刚测试了 MSVS,它的行为类似于 g++。三个主要供应商都犯了这个错误,这有点令人担忧。
  • 我的 DR 只是关于“int a = 5; int b = a++”之类的情况。我当时提出了一个相应的SO问题。但是,我没有就支撑的初始化列表制作 DR,我也没有对此问题中示例的预期规则有什么看法。至少在 90 年代的草稿和最近的发展中进行了一次调查:)
  • 这里是DR基于stackoverflow.com/questions/5760866/…的问题
  • @ecatmur 你指的是术语“constexpr 函数”和“constexpr 构造函数”吗? “constexpr 函数”不是 constexpr 函数,但“constexpr 函数”是一个已定义的非复合术语:“在非构造函数的声明中使用的 constexpr 说明符将该函数声明为 constexpr 函数。"所以当 expr.const 说“constexpr function”时,它并没有说“function that is constexpr”,而是实际上指的是“constexpr function”的定义。
  • 请注意,expr.const 本身的第二个要点是“调用除 constexpr 构造函数之外的函数”,这说明构造函数在该段落中被视为函数。
猜你喜欢
  • 1970-01-01
  • 2019-02-08
  • 2014-08-26
  • 2020-09-05
  • 2023-02-07
  • 1970-01-01
  • 1970-01-01
  • 2012-09-22
  • 2013-11-20
相关资源
最近更新 更多