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

Why doesn't a const reference extend the life of a temporary object passed via a function?

在下面的简单示例中,为什么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
}

live example - 产生:

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;
}

live example 产生:

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

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

                                      ^

Compiler returned: 1

这是故意的。引用只能在它绑定到临时直接时才能延长临时的生命周期。在您的代码中,您将 ref2 绑定到 min 的结果,这是一个引用。该引用引用临时文件并不重要。只有 b 延长了临时的生命周期; ref2 也指的是同一个临时文件并不重要。

另一种看待它的方式:您不能选择延长生命周期。这是一个静态 属性。如果 ref2 会做正确的事情 tm,那么根据 xy+1 的运行时值,生命周期是否延长。不是编译器能做的。

这是设计使然。简而言之,只有 named 临时对象 直接 绑定的引用才会延长其生命周期。

[class.temporary]

5 There are three contexts in which temporaries are destroyed at a different point than the end of the full-expression. [...]

6 The third context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:

  • A temporary object bound to a reference parameter in a function call persists until the completion of the full-expression containing the call.
  • The lifetime of a temporary bound to the returned value in a function return statement is not extended; the temporary is destroyed at the end of the full-expression in the return statement.
  • [...]

您没有直接绑定到 ref2,您甚至通过 return 语句传递它。该标准明确表示不会延长使用寿命。部分是为了使某些优化成为可能。但归根结底,因为在将引用传入和传出函数时跟踪应该扩展哪个临时文件通常是很棘手的。

由于编译器可能会假设您的程序没有未定义的行为而进行积极优化,因此您会看到这种情况的可能表现。在其生命周期之外访问一个值是未定义的,这就是 return ref2; 所做的 ,并且由于行为未定义,简单地 returning 零是一个有效的表现行为.编译器没有破坏契约。

我会先回答问题,然后提供一些答案的背景。 The current working draft contains the following wording:

The temporary object to which the reference is bound or the temporary object that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference if the glvalue to which the reference is bound was obtained through one of the following:

  • a temporary materialization conversion ([conv.rval]),
  • ( expression ), where expression is one of these expressions,
  • subscripting ([expr.sub]) of an array operand, where that operand is one of these expressions,
  • a class member access ([expr.ref]) using the . operator where the left operand is one of these expressions and the right operand designates a non-static data member of non-reference type,
  • a pointer-to-member operation ([expr.mptr.oper]) using the .* operator where the left operand is one of these expressions and the right operand is a pointer to data member of non-reference type,
  • a const_­cast ([expr.const.cast]), static_­cast ([expr.static.cast]), dynamic_­cast ([expr.dynamic.cast]), or reinterpret_­cast ([expr.reinterpret.cast]) converting, without a user-defined conversion, a glvalue operand that is one of these expressions to a glvalue that refers to the object designated by the operand, or to its complete object or a subobject thereof,
  • a conditional expression ([expr.cond]) that is a glvalue where the second or third operand is one of these expressions, or
  • a comma expression ([expr.comma]) that is a glvalue where the right operand is one of these expressions.

据此,当引用绑定到从函数调用返回的glvalue时,不会发生生命周期延长,因为glvalue是从函数调用中获得的,这不是生命周期延长的允许表达式之一.

绑定到引用参数 b 时,y+1 临时文件的生命周期延长一次。在这里,prvalue y+1 被物化为一个 xvalue,引用绑定到临时物化转换的结果;寿命延长因此发生。然而,当min函数returns时,ref2绑定到调用的结果,这里不会发生生命周期延长。因此,y+1 临时对象在 ref2 定义的末尾被销毁,并且 ref2 成为悬空引用。


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

人们经常声称只有当引用将 "directly" 绑定到临时对象时,生命周期延长才适用,但标准从未对此做出任何说明。实际上,该标准定义了引用 "bind directly" 的含义,而该定义( 例如 const std::string& s = "foo"; 是间接引用绑定)显然与此处无关.

Rakete1111 在 SO 的其他地方的评论中说,生命周期延长仅适用于引用绑定到纯右值(而不是通过先前引用绑定到该临时对象获得的一些泛左值); "bound ... directly" 他们似乎在说类似的话。然而,没有文本支持这一理论。事实上,像下面这样的代码有时被认为会触发生命周期延长:

struct S { int x; };
const int& r = S{42}.x;

然而,在 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)。从某种意义上说,它阐明了引用必须绑定 "directly" 的直觉,方法是描述绑定 "direct" 足以使生命周期延长发生的情况;但是,它并没有限制到仅在引用绑定到纯右值时才允许它。它允许在 S{42}.x 情况下延长生命周期。

[答案应该更新,因为非constexpr版本实际上编译]

constexpr版本

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

解释: y + 1 生成的 rvalue 的生命周期实际上延长了。这是因为 minreturn 类型是对常量的引用,即 const T& 并且只要您有对常量的引用 直接绑定 rvalue 类型,基础 rvalue 值的生命周期被延长到 reference-to-const 存在。

更进一步,min的reference-to-const输出直接赋值给类型为const int&的左值名称ref2int 类型也应该在这里工作(即 int ref2 = min(x, y+1);),在这种情况下,基础 rvalue 将被复制,并且对 const 的引用将被销毁。

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

constexpr版本

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

但是,您应该可以通过以下方式轻松解决此问题:

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