【问题标题】:The point of destroying a temporary object when it created in a member-initializer在成员初始化程序中创建临时对象时销毁它的点
【发布时间】:2021-04-03 07:47:40
【问题描述】:
#include <iostream>
struct A{
    A(int){
       
    }
    ~A(){
        std::cout<<"A destroy\n";
    }
};
struct B{
    B(int){
        std::cout<<"B construct\n";
    }
    ~B(){
        std::cout<<"B destroy\n";
    }
};
struct Content{
    A const& a;
};
struct Data{
    Data():c{0},b{0}{

    }
    Content c;
    B b;
};
int main(){
    Data d;
    std::cout<<"exit\n";
}

GCC的输出是:

B construct
A destroy
exit
B destroy

Clang 抱怨这段代码格式不正确。 Here 是两个编译器的性能。

关于Clang报错,标准中确实有相关规定,即:
[class.init#class.base.init-8]

绑定到 mem-initializer 中的引用成员的临时表达式格式不正确。

我不确定 Clang 是否理解过度了?在我看来,规则似乎是说,由 mem-initializer 的 mem-initializer-id 命名的引用成员不应绑定到临时表达式。在我的示例中,Data 类的成员 c 不是引用。

据推测,Clang 认为任何使引用成员绑定到临时表达式的引用成员初始化都发生在成员初始化器中,都是格式错误的。所以我举了一个例子来检验Clang是否这么认为。

struct A{
  int const& rf;
};
struct B{
   B():a(new A{0}){}
   A* a;
};
int main(){
  B b;
  delete b.a;
}

gives 是警告,但不是错误。所以,我不确定Clang 是否这么认为。不知道它是怎么理解规则的?

如果第一个例子本身是有效的,我会认为GCC 不符合标准。因为销毁临时对象的顺序。

[class.temporary#4]

临时对象在评估完整表达式 ([intro.execution]) 的最后一步被销毁,该完整表达式 (lexically) 包含它们的创建点。

临时对象将在成员初始化器的完整表达式末尾被销毁,在我的示例中,即c{0}。但是,GCC 会在子对象b 构造完成后销毁临时对象。我认为这是第一个问题。

其实临时绑定的引用也不例外:
[class.temporary#6]

引用绑定到的临时对象或作为引用绑定到的子对象的完整对象的临时对象在引用的生命周期内持续存在
此生命周期规则的例外是:

  • 在函数调用 ([expr.call]) 中绑定到引用参数的临时对象一直存在,直到包含调用的完整表达式完成为止。
  • 绑定到从带括号的表达式列表 ([dcl.init]) 初始化的类类型聚合的引用元素的临时对象将持续存在,直到包含表达式列表的完整表达式完成为止。
  • 临时绑定到函数返回语句 ([stmt.return]) 中的返回值的生命周期未延长;临时在 return 语句中的完整表达式的末尾被销毁。
  • 在 new-initializer ([expr.new]) 中的引用的临时绑定一直存在,直到包含 new-initializer 的完整表达式完成为止。

也不是,也就是说,我的第一个例子不是上面列表中列出的例外。因此,临时对象的生命周期应该与对象b的子对象c的子对象a的生命周期相同,它们的生命周期都与b的生命周期相同。那么,为什么GCC 这么早就销毁临时对象呢?临时对象不应该和main中的对象b一起销毁吗?我想这是GCC的第二期。我不知道Clang如何处理临时对象,因为它之前已经报错了。

问题:

  1. Clang 报告第一个示例的错误吗?如果是正确的,[class.init#class.base.init-8] 是否应该更清楚?

  2. 如果Clang过度理解[class.init#class.base.init-8],那么GCC销毁临时对象的表现是否算是bug?或者,exceptions 忽略了这个案例?即使异常省略了这种情况(引用绑定发生在成员初始化器中),我仍然认为GCC有错误,临时不应该在完整表达式(c{0})的末尾被销毁,这是在构造之前排序的b.

如何解读以上这些问题?

【问题讨论】:

  • 这是导致问题的c{0} - ca 成员使用对临时 A 对象的引用进行初始化。
  • @1201ProgramAlarm 这会引起什么问题吗?是否违反任何规则? c{0} 将执行聚合初始化。这里的关键点是临时A 对象的生命周期
  • @1201ProgramAlarm 我不确定你是否引用了this example。这种情况也不例外,它具有销毁临时对象的正确行为。

标签: c++ gcc clang language-lawyer


【解决方案1】:

Clang 是正确的,但是是的,标准可能更清晰。

[class.temporary]/6(引用绑定的临时表达式生命周期延长)的作用是确保除了列出的异常之外,绑定到引用的临时对象的生命周期延长到引用的生命周期。但是,当引用是类非静态数据成员时,引用的生命周期在绑定点(这发生在(可能是默认的)构造函数中)不是静态已知的,因此临时可以延长寿命。由于非静态数据成员不包括在异常列表中,因此必须通过其他方式防止这种情况发生,事实上,[class.base.init] 中的 IF 案例是这样的:

8 - 绑定到 mem-initializer 中的引用成员的临时表达式格式不正确。

11 - 从默认成员初始化程序绑定到引用成员的临时表达式格式不正确。

我们必须得出结论,这种语言的意图是呈现格式错误的任何尝试从类的(可能是默认的)构造函数中将临时绑定到类数据成员,否则临时将有资格延长生命周期,这将是荒谬的(其中引用的生命周期是静态未知的)。因此,这必须包括子聚合的参考成员;标准中包含一个说明这一点的说明是合适的。

值得考虑将临时表达式绑定到引用属于 3 种情况:

  1. IF 在 [class.base.init]/8 和 /11 下(类非静态数据成员)
  2. [class.temporary]/6 下的异常;临时在完整表达式结束时被销毁。
  3. 否则会延长使用寿命。

因此,如果编译器不拒绝,并且代码不属于 [class.temporary]/6 中的异常之一,并且编译器不执行生命周期扩展(到引用的完整生命周期),编译器有问题。

MSVC 也错误地accepts 你的代码和输出:

main
A(int)
~A()
B(int)
D()
exit
~B()

一个有趣的情况是,包含引用非静态数据成员的类是一个聚合,因此有资格通过列表初始化语法进行聚合初始化(示例改编自 CWG 1815):

struct A {};
struct C { A&& a = A{}; };
C c1;         // #1
C c2{A{}};    // #2
C c3{};       // #3
C c4 = C();   // #4

这里 #1#4 格式不正确,尽管 gcc 和 MSVC 错误地接受。 #3 按照 [class.base.init]/11 标准的当前措辞格式不正确,但这与 CWG 1815 中指出的委员会的意图相反:

2014 年 2 月会议记录:

CWG 同意建议的方向,将示例中的 #3 视为 #2 并删除默认构造函数

也就是说,#3 将是有效的,并导致生命周期延长; gcc and clang do so, but MSVC fails to perform lifetime extension in either #2 or #3; icc performs lifetime extension for #2 only。奇怪的是,clang 报告了一个诊断,声称它不会执行生命周期延长尽管这样做!

警告:抱歉,不支持使用默认成员初始化程序聚合初始化创建的临时的生命周期延长;临时的生命周期将在完整表达式结束时结束 [-Wdangling]

我已经问过Validity and/or lifetime extension of mem-initializer in aggregate initialization

请注意(假设这是允许的)它仅在 c3 是一个完整对象时才有效;如果它是一个成员对象(如您的第一个示例中的c)[class.base.init] 将明确适用,c3 也将是格式错误的。

【讨论】:

  • [class.temporary]/6 段落中的措辞在哪里? “当绑定到临时对象的引用是存在于函数范围内的对象时(因此在其范围结束之前具有生命周期,由大括号或控制流构造分隔)或命名空间范围内(等等具有整个程序的生命周期)。”虽然可以直观的理解,但是相关的写法在哪里呢?
  • @jackX 谢谢,我修改了我的答案。你再看看好吗?
  • 我认为c3 仍然应该是无效的,因为它服从dcl.init.aggr#8.1。即:如果元素具有默认成员初始化程序 ([class.mem]),则元素从该初始化程序初始化。因此,[class.base.init]/11 也适用于此。
  • @jackX 一个很好的观点,但有点令人困惑。 CWG 1815 的注释说,在 Issaquah (2014-02) CWG 决定使代码 c3{} 格式正确;然后在 Rapperswil (2014-06),DR 被 CWG 1696 的决议标记为已解决 - 但采用的语言没有举例说明这种情况是否被允许,而且使c3{} 书面形式不正确,但 CWG 的意图肯定很重要,更不用说实施实践了?我会更新我的答案并继续努力。
猜你喜欢
  • 2012-03-03
  • 1970-01-01
  • 1970-01-01
  • 2023-03-21
  • 2013-07-13
  • 2014-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多