为什么通过函数传递的临时对象,即使使用const引用也不能延长其生命周期?

50
在下面的简单示例中,为什么不能将 ref2 绑定到 min(x,y+1) 的结果?
#include <cstdio>
template< typename T > const T& min(const T& a, const T& b){ return a < b ? a : b ; }

int main(){
      int x = 10, y = 2;
      const int& ref = min(x,y); //OK
      const int& ref2 = min(x,y+1); //NOT OK, WHY?
      return ref2; // Compiles to return 0
}

实时示例 - 生成:

main:
  xor eax, eax
  ret

编辑: 下面的例子更能描述这种情况。

#include <stdio.h>


template< typename T >
constexpr T const& min( T const& a, T const& b ) { return a < b ? a : b ; }



constexpr int x = 10;
constexpr int y = 2;

constexpr int const& ref = min(x,y);  // OK

constexpr int const& ref2 = min(x,y+1); // Compiler Error

int main()
{
      return 0;
}

实时示例 生成的结果如下:

<source>:14:38: error: '<anonymous>' is not a constant expression

 constexpr int const& ref2 = min(x,y+1);

                                      ^

Compiler returned: 1

9
该程序没有输出,并以0代码退出。使用“-O3”优化标志时,主函数中的所有语句都将被丢弃。 - user7860670
4
它报什么错误? - fredrik
7
这实际上是一个非常有趣的问题。我会标记语言法律专家,并希望其中一位专家能够接手处理。我既没有时间也缺乏专业知识。这与寿命延长不可传递以及原始对象所在位置有关。继续@StoryTeller。 - Bathsheba
6
@Bathsheba,您能否简要解释一下为什么b < a ? b : a很有优势? - lubgr
5
问题是,如果绑定是直接的(例如,如果 min 返回值),ref2 将会将绑定的临时变量的生命周期延长到它自己的生命周期。所以我猜这就是问题的关键。 - Angew is no longer proud of SO
显示剩余7条评论
4个回答

44
这是设计上的考虑。简而言之,只有直接绑定到临时对象的命名引用才会延长其生命周期。
[class.temporary]规范中指出,有三种情况下临时对象会在不同于完整表达式结束点被销毁。第三种情况是将引用绑定到临时对象上。除非以下情况,与引用绑定的临时对象或者是引用绑定的子对象的完整临时对象将在引用的生命周期内保留:
1. 作为函数调用中引用参数绑定的临时对象将持久到包含该调用的完整表达式完成之后。 2. 函数返回语句中绑定到返回值的临时对象的生命周期不会被延长;该临时对象将在返回语句的完整表达式结束时被销毁。 3. [...]
你没有直接绑定到ref2,并且甚至通过返回语句传递它。标准明确表示不会延长其生命周期。部分原因是为了使某些优化变得可能,但最终原因是因为一般情况下跟踪哪个临时对象应该被延长当引用进出函数时是棘手的。

由于编译器可能会进行激进的优化,假设您的程序不会出现未定义行为,因此您会看到这种可能的表现。在生命周期外访问值是未定义的,这就是 return ref2; 所做的,并且由于行为未定义,简单地返回零是一种有效的行为。编译器没有违反任何契约。


1
非常感谢。我错误地认为 y+1 的临时对象绑定到了 b 上,并且它的生命周期通过返回函数延长。 - Khurshid Normuradov

19

这是有意为之的。当引用直接绑定到临时对象上时,引用才能延长临时对象的生命周期。在你的代码中,你将 ref2 绑定到了 min 的结果上,而该结果是一个引用。虽然该引用引用了一个临时对象,但这并不重要。只有 b 才能延长该临时对象的生命周期;ref2 引用同一个临时对象并不重要。

另一种解释:你不能选择性地进行生命周期延长。它是一个静态属性。如果 ref2 能够在正确的时间做出正确的事情tm,那么根据 xy+1 的运行时值,生命周期就会被延长或者没有被延长。这是编译器无法处理的。


10

首先,我将回答问题,然后提供答案的一些背景信息。根据最新工作草案的规定,只有在以下情况下获取到的glvalue绑定到的临时对象或者是绑定到子对象的完整对象会持续到引用生命周期结束:

如果引用所绑定的glvalue是通过以下方式之一获取的:

  • 临时物化转换([conv.rval]),
  • ( expression ),其中expression是下列表达式之一,
  • 数组操作符([expr.sub])的下标,其中操作数为以下表达式之一,
  • 使用左操作数为以下表达式且右操作数指定非引用类型的非静态数据成员的.运算符进行的类成员访问([expr.ref]),
  • 使用左操作数为以下表达式且右操作数为非引用类型的数据成员指针的指针成员操作([expr.mptr.oper])的.*运算符,
  • 对glvalue操作数进行const_­cast([expr.const.cast])、static_­cast([expr.static.cast])、dynamic_­cast([expr.dynamic.cast])或reinterpret_­cast([expr.reinterpret.cast])转换,将其中一种表达式的glvalue操作数转换为指向指定操作数所指定对象、其完整对象或子对象的glvalue,而不需要用户定义的转换,
  • 是一个glvalue的条件表达式([expr.cond]),其中第二个或第三个操作数是以下表达式之一,或者
  • 是一个glvalue的逗号表达式([expr.comma]),其中右操作数是以下表达式之一的情况下。

根据这个规定,当引用绑定到从函数调用返回的glvalue时,寿命扩展不会发生,因为该glvalue是从函数调用中获取的,而函数调用不是允许的生存期延长表达式之一。

y+1临时对象的寿命在绑定到引用参数b时得到了扩展。在这里,prvalue y+1 被实现以产生 xvalue,并将引用绑定到临时物化转换的结果;因此生命周期延长发生了。然而,当min函数返回时,ref2绑定到调用结果,生命周期延长在这里不会发生。因此,y+1临时对象在ref2定义结束时被销毁,并且ref2成为悬空引用。


在这个话题上曾经存在一些混淆。众所周知,OP的代码和类似的代码会导致悬空引用,但是标准文本,即使在C++17之前,也没有提供明确的解释。

通常被认为寿命扩展仅适用于引用“直接”绑定到临时对象的情况,但标准从未说过这样的话。实际上,标准定义了引用“直接”绑定的含义,并且该定义(例如,

<code>struct S { int x; };
const int& r = S{42}.x;
</code>
然而,在C++14中,表达式S{42}.x成为了一个xvalue,因此如果在这里应用生命周期延长,则不是因为引用绑定到了prvalue上。有人可能会声称,生命周期延长仅适用一次,并且将任何其他引用绑定到相同对象不会进一步延长其生命周期。这就解释了为什么OP的代码会创建悬空引用,而不会阻止S{42}.x情况下的生命周期延长。然而,在标准中并没有这样的规定。StoryTeller在这里也说过,引用必须直接绑定,但我不知道他的意思。他引用了标准文本表明,在return语句中将引用绑定到临时变量不会延长其生命周期。然而,该语句似乎旨在适用于在return语句中创建临时变量的完整表达式的情况,因为它说临时变量将在该完整表达式的末尾被销毁。显然,对于y+1 的临时变量来说,情况并非如此,它将在包含对min调用的完整表达式结束时被销毁。因此,我倾向于认为这个声明不适用于类似问题中的情况。相反,它的效果,加上生命周期延长的其他限制,是防止任何临时对象的生命周期超出其创建的块作用域。但这并不能阻止问题中的y+1临时变量存活到main的末尾。因此,问题仍然存在:是什么原则解释了为什么将ref2绑定到问题中的临时变量不会延长该临时变量的生命周期?我之前引用的当前工作草案的措辞是由CWG 1299的解决方案引入的,该方案于2011年开放,但直到最近才得到解决(未能及时解决C++17)。在某种意义上,它通过划分那些绑定足够“直接”的情况,使生命周期延长发生;然而,它并不如此严格,只允许在引用绑定到prvalue时进行。它允许在S{42}.x情况下进行生命周期延长。

0

[由于非constexpr版本实际上可以编译,因此答案应该更新]

constexpr版本

演示:https://godbolt.org/z/_p3njK

解释:所讨论的rvalue的生命周期实际上被延长了。这是因为minreturn类型是引用到常量,即const T&,每当您将引用到常量直接绑定到一个rvalue类型时,底层rvalue值的生命周期会延长到reference-to-const存在。

进一步说,min 的常量引用输出随后直接分配给 lvalue 名称 ref2,其类型为 const int&;这里也可以使用 int 类型(即 int ref2 = min(x, y+1);),在这种情况下,基础的 rvalue 将被复制并销毁常量引用。

总之,在符合现代 C++ 标准的最新编译器版本中,非 constexpr 版本应始终产生所需的输出。

constexpr 版本

这里的问题不同,因为 ref2 的类型说明符要求它是一个 constexpr,这反过来又要求表达式是一个编译时字面值。虽然理论上可以在这里应用生命周期延长以用于 constexpr 常量引用类型,但 C++ 目前还不允许这样做(即它不创建临时的 constexpr 类型来保存基础的 rvalues),可能是因为它禁止了某些优化或使编译器的工作更加困难 - 不确定是哪一个。

然而,你应该能够轻松地解决这个问题:

constexpr int value = min(x, y + 1);
constexpr int const& ref2 = value;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接