【问题标题】:About binding a const reference to a sub-object of a temporary关于将 const 引用绑定到临时的子对象
【发布时间】:2016-03-11 18:23:50
【问题描述】:

像这样的代码

#include <stdio.h>

struct P2d {
    double x, y;
    P2d(double x, double y) : x(x), y(y) {}
    ~P2d() { printf("Destructor called\n"); }
};

P2d center() {
    return P2d(10, 10);
}

int main(int argc, const char *argv[]) {
    const double& x = center().x;
    printf("x = %.18g\n", x);
    return 0;
}

g++ (版本 5.2.0) 将销毁P2d 临时实例 进入main 中的printf,但无论如何都会保留该值(即代替绑定x 到临时 P2d 实例的实际成员,它将创建 另一个临时 double 以复制成员的值)。

clang++(IMO 正确)而是将临时 P2d 实例的生命周期延长到 x 引用的生命周期,因此析构函数将在 printf 之后调用main.

如果不是使用普通的double 作为xy 成员的类型,而是创建一个类(例如Double),那么两个编译器都同意并将临时P2d 对象的生命周期延长到过去的printf

这是g++ 中的错误还是标准允许的错误?

【问题讨论】:

  • 在 2021 年 2 月 24 日在gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 上测试了这个程序,它打印了x = 10, Destructor called,所以必须“修复”。也会尝试 MSVC。
  • 在同一天使用 msvc Microsoft (R) C/C++ Optimizing Compiler Version 19.26.28805 for x64 测试,即使使用 /std:c++17,msvc 也会错误地打印 Destructor called, x = 10

标签: c++ language-lawyer


【解决方案1】:

这被CWG 1651覆盖:

问题的解决6161213,制作会员的结果 应用于纯右值的访问或下标表达式是一个 xvalue,表示 绑定对此类临时子对象的引用不会 延长临时工的寿命。 12.2 [class.temporary] 应该是 修改以确保它确实如此。

现状是only prvalues are treated as referring to temporaries - 因此[class.temporary]/5“第二个上下文是引用绑定到临时的。”)被认为不适用。不过,Clang 和 GCC 并没有真正实现 issue 616 的解决方案。 center().x is treated as a prvalue by both。我的最佳猜测:

  • GCC 根本没有对任何 DR 做出反应。使用标量子对象时它不会延长生命周期,因为[dcl.init.ref]/(5.2.1.1) 涵盖了。所以完整的临时对象不需要继续存在(参见aschelper's answer),它不需要,因为引用不直接绑定。如果子对象是类或数组类型,则引用直接绑定,GCC 会延长临时对象的生命周期。这已在DR 60297 中注明。

  • Clang 识别成员访问权限并已经实施了“新的”生命周期延长规则 - 甚至是 handles casts。从技术上讲,这与它处理值类别的方式不一致。但是,一旦解决了上述 DR,这将是更明智的行为。

因此,我会说 GCC 的当前措辞是正确的,但当前措辞有缺陷且含糊不清,并且 Clang 已经实施了 DR 1651 的未决解决方案,即N3918。本文非常清楚地介绍了这个例子:

如果E1 是一个临时表达式并且E2 没有指定一个 位域,则E1.E2 是一个临时表达式。

center() 是根据论文对 [expr.call]/11 的措辞的临时表达。因此,它在上述 [class.temporary] /5 中的修改措辞适用:

第二个上下文是引用不直接绑定时(8.5.3 dcl.init.ref) 或用临时表达式初始化(第 5 条)。 对应的临时 对象(如果有的话)在引用的生命周期内持续存在,除了:[...不适用的异常...]

瞧,我们可以延长寿命。注意“对应的临时对象”不够明确,是提案延期的原因之一;修改后肯定会被采纳的。


是一个 xvalue(但不是位域)、类纯右值、数组纯右值或函数左值,“cv1 T1”与“cv2 T2”引用兼容,或者 [... ]

事实上,GCC 完全尊重这一点,如果子对象具有数组类型,它将延长生命周期。

【讨论】:

  • 这个措辞会被采纳吗?它不在最新的工作草案中。
  • @Barry 该提案存在一些小问题;见discussion of CWG 1299。我非常肯定修改后的论文将被接受。我会向作者发送消息。
  • 那么8.5.3中关于引用初始化的一般说明不适用? center() AFAIK 是 prvalue,我假设 prvalue 的子对象也是 prvalue。鉴于在示例中不是“类纯右值”,在我看来,8.5.3 中的措辞规定创建一个临时的double 并绑定到它而不是绑定到成员子对象。
  • @6502 不,center().x 是一个 xvalue。
  • @Columbo:哪里说center().xxvalue?定义说“一个 xvalue 是某些涉及右值引用的表达式的结果”。在这种情况下,右值引用在哪里?
【解决方案2】:

我会争论 g++ 中的一个错误,因为引用 draft N3242, §12.2/5:

第二个上下文是引用绑定到临时的。引用的临时对象 绑定或作为引用所绑定的子对象的完整对象的临时对象在引用的生命周期内持续存在,除了:

所以它的生命周期必须延长,除非:

在构造函数的 ctor-initializer [..] 中临时绑定到引用成员 [..]

在函数调用中临时绑定到引用参数 [..]

临时绑定到函数返回语句中的返回值的生命周期 [..]

临时绑定到 new-initializer [..] 中的引用

我们的案例不符合任何这些例外,因此它必须遵守规则。我会说 g++ 在这里是错误的。

然后,关于 aschepler 从同一草案 §8.5.3/5(强调我的)中提出的引述:

对“cv1T1”类型的引用由“cv2T2”类型的表达式初始化,如下所示:

  1. 如果引用是左值引用和初始化表达式

    一个。是一个左值(但不是位域)并且“cv1T1”与“cv2T2”引用兼容,或者

    b.有一个类类型...

    然后...

  2. 否则,引用应为对非易失性 const 类型的左值引用(即,cv1 应为 const),或者引用应为右值引用。

    一个。如果 初始化表达式

    • 我。 是一个 xvalue、类纯右值、数组纯右值或函数左值,并且“cv1T1”与“cv2T2 引用兼容",或

    • 二。有一个类类型...

    那么引用就绑定到第一种情况下的初始化表达式的值....

    b.否则,“cv1 T1”类型的临时变量会使用非引用复制初始化 (8.5) 的规则从初始化表达式创建和初始化。然后将引用绑定到临时对象。

看看什么是xvalue,这次引用http://en.cppreference.com/w/cpp/language/value_category ...

一个 xvalue(“过期值”)表达式是 [..]

a.m,对象表达式的成员,其中a为右值,m为非引用类型的非静态数据成员;

...表达式center().x 应该是一个xvalue,因此适用第8.5.3/5节中的案例2a(并且不是副本)。我会坚持我的建议:g++ 是错误的。

【讨论】:

  • 在描述xvalue 的表中指出,返回非引用的函数调用是prvalue。所以center()prvalue,而且我认为center().x 也是prvalue,而不是xvalue。显然,其意图是将xvalues 仅用于移动施工案例...
  • 我怀疑:center() 是纯右值,它是右值。此外,x 是一个非静态数据成员。因此,我引用的部分适用。也许这有帮助:从2.0 + 2.0 返回的双精度是临时的,但它没有“身份”;这只是价值。 center().x 也是一个临时的 double,但它一个身份,它是 P2d 的 double 数据成员之一。
  • N3242 是古老的,你为什么要引用它
  • @MM 这个问题没有用特定标准标记,所以我想“保持低调”并继续使用 C++11(它还引入了 xvalue 的概念,所以)
  • 除非有理由专门使用 C++11,否则最好使用 C++14 草案 (N4140);从那时起,许多缺陷已得到修复
【解决方案3】:

请阅读Columbo's answer


这是一个 gcc 错误。相关规则在[class.temporary]

在两种情况下,临时对象在与完整表达式结尾不同的点被销毁。 [...]

第二个上下文是引用绑定到临时的。引用绑定到的临时对象或作为引用绑定到的子对象的完整对象的 临时对象仍然存在 在参考的生命周期内除了:
— 在函数调用 (5.2.2) 中绑定到引用参数的临时对象将持续存在直到完成 包含调用的完整表达式。
— 在函数返回语句 (6.6.3) 中临时绑定到返回值的生命周期不是 扩展;临时在 return 语句中的完整表达式的末尾被销毁。
— 临时绑定到 new-initializer (5.3.4) 中的引用,直到完成 包含 new-initializer 的完整表达式。

我们将引用绑定到临时对象的子对象,因此临时对象应该在引用的生命周期内持续存在。此规则的这三个例外均不适用于此处。

【讨论】:

  • 但是根据 8.5.3,我相信引用实际上并没有绑定到临时类的子对象 - 请参阅我的答案。
猜你喜欢
  • 2016-11-09
  • 2019-07-02
  • 2012-07-18
  • 2018-08-14
  • 1970-01-01
  • 2019-12-21
  • 1970-01-01
相关资源
最近更新 更多