一个const引用类成员会延长临时对象的生命周期吗?

198
为什么会这样呢?
#include <string>
#include <iostream>
using namespace std;

class Sandbox
{
public:
    Sandbox(const string& n) : member(n) {}
    const string& member;
};

int main()
{
    Sandbox sandbox(string("four"));
    cout << "The answer is: " << sandbox.member << endl;
    return 0;
}

给出输出结果:
答案是:
而不是:
答案是:四

51
如果你写成了 cout << "The answer is: " << Sandbox(string("four")).member << endl;,那么就可以保证它能够正常工作,而且更加有趣。 - Roger Pate
8
@RogerPate 你能解释一下为什么吗? - Paolo M
25
对于一个好奇的人来说,Roger Pate的例子很有用,因为*string("four")*是临时的,并且这个临时变量会在完整表达式结束时被销毁,所以在他的例子中,当读取SandBox::member时,临时的字符串仍然存在 - PcAF
2
问题是:由于编写这样的类很危险,是否有编译器警告防止将临时变量传递给这样的类,或者是否有设计指南(在Stroustrup中?)禁止编写存储引用的类?与其存储引用,更好的设计指南是存储指针。 - Grim Fandango
2
就此而言,我无法在GCC或MSVC 2013上重现“答案是:”的输出。这通常需要-O3或其他什么才能显示出来吗? - jrh
显示剩余4条评论
6个回答

191

只有本地的const引用可以延长生命周期。

标准在§8.5.3/5 [dcl.init.ref]中的引用声明初始化器部分规定了这种行为。在您的示例中,引用被绑定到构造函数的参数n,当对象n绑定的范围超出范围时,引用将无效。

生命周期延长不会通过函数参数传递。 §12.2/5 [class.temporary]:

第二种情况是当引用绑定到临时变量时。引用所绑定的临时变量或者完整对象的子对象会持续到引用的生命周期结束,除非如下所述。在构造函数的ctor-initializer(§12.6.2 [class.base.init])中绑定到引用成员的临时变量将持续到构造函数退出。在函数调用中绑定到引用参数的临时变量(§5.2.2 [expr.call])将持续到包含该调用的完整表达式完成。


56
请参考GotW #88,以获得更通俗易懂的解释:http://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/。这可以帮助你更好地理解本文所讲述的内容。 - Nathan Ernst
1
我认为如果标准规定“第二种情况是当引用绑定到 prvalue 时”,将会更加清晰。在 OP 的代码中,你可以说 member 绑定到了一个临时变量,因为用 n 初始化 member 意味着将 member 绑定到与 n 绑定的相同对象上,而在这种情况下,实际上是一个临时对象。 - M.M
2
@M.M 在某些情况下,包含 prvalue 的 lvalue 或 xvalue 初始化程序将扩展 prvalue。我的提案文件 P0066 回顾了现状。 - Potatoswatter
1
从C++11开始,Rvalue引用也可以延长临时对象的生命周期,而不需要使用const限定符。 - GetFree
3
@KeNVinFavo 是的,使用已死对象始终是未定义行为。 - Potatoswatter
显示剩余5条评论

32

以下是最简单的解释:

在main()函数中,您创建了一个字符串并将其传递给构造函数。这个字符串实例只存在于构造函数中。在构造函数中,您分配了成员指向该实例。当作用域离开构造函数时,该字符串实例被销毁,成员然后指向一个不存在的字符串对象。让Sandbox.member指向其作用域外的引用不会使这些外部实例保持在作用域中。

如果您想修复程序以显示所需的行为,请进行以下更改:

int main()
{
    string temp = string("four");    
    Sandbox sandbox(temp);
    cout << sandbox.member << endl;
    return 0;
}

现在,temp将在main()结束时而不是构造函数结束时失效。但这是一种不好的做法。你的成员变量不应该是一个指向存在于实例之外的变量的引用。实际上,你永远不知道那个变量何时会失效。

我建议将Sandbox.member定义为const string member; 这将把临时参数的数据复制到成员变量中,而不是将成员变量赋值为临时参数本身。


如果我这样做:const string & temp = string("four"); Sandbox sandbox(temp); cout << sandbox.member << endl; 它还能正常工作吗? - Yves
@Thomas const string &temp = string("four");const string temp("four"); 会得到相同的结果,除非你明确使用了 decltype(temp) - M.M
@M.M 非常感谢,现在我完全理解这个问题了。 - Yves
然而,这是不好的实践。为什么?如果临时变量和包含对象在同一作用域中都使用自动存储,那么它不是100%安全吗?如果不这样做,如果字符串太大,复制成本太高,你会怎么做? - max
2
@max,因为该类不强制要求传入的临时变量具有正确的作用域。这意味着有一天你可能会忘记这个要求,传递无效的临时值,而编译器不会警告你。 - Alex Che

5
从技术上讲,此程序并不需要将任何内容输出到标准输出(它本身就是一个缓冲流)。
  • cout << "The answer is: " 会将 "The answer is: " 输出到 stdout 的缓冲区。

  • 然后,<< sandbox.member 将悬空引用提供给 operator << (ostream &, const std::string &),从而导致未定义行为

因此,没有什么是保证会发生的。程序可能看起来工作良好,也可能在没有刷新 stdout 的情况下崩溃 -- 这意味着文本 "The answer is: " 不会出现在您的屏幕上。

4
当存在未定义行为时,整个程序的行为都是未定义的 - 它不仅会在执行的特定点开始。因此,我们不能确定哪里将写入“答案是:”。 - Toby Speight

4
从其他答案可以清楚地看出,类成员不会延长临时对象的生命周期超出构造函数调用。但是,在某些情况下,您的API可以“安全”地假设传递给类的所有const&对象都不是临时对象,而是引用到良好作用域的对象。
如果您不想创建副本,该怎么办才能确保UB不会潜入您的代码中呢?您拥有的最佳工具是通过声明接受这种临时对象的重载为已删除来保护假设传递给构造函数的std::string const&不是临时对象:
#include <string>
#include <iostream>
using namespace std;

class Sandbox
{
public:
    Sandbox(const string& n) : member(n) {}
    
    Sandbox(string&&) = delete;
    // ^^^ This guy ;) 

    const string& member;
};

int main()
{
    Sandbox sandbox(string("four"));
    // Detect you're trying ^^^ to bind a 
    // reference to a temporary and refuse to compile

    return 0;
}

Demo


0

因为一旦Sandbox构造函数返回,您的临时字符串就超出了作用域,并且其所占用的堆栈被回收用于其他目的。

通常情况下,您不应该长期保留引用。引用适用于参数或局部变量,而不是类成员。


7
“从来没有”这个词非常绝对。 - Fred Larson
18
除非您需要保留对对象的引用,否则永不使用类成员。有时您需要保留对其他对象的引用,而不是副本,在这种情况下,引用比指针更清晰地解决问题。 - David Rodríguez - dribeas

-1

你正在引用已经消失的内容。以下方法可以解决问题。

#include <string>
#include <iostream>

class Sandbox
{

public:
    const string member = " "; //default to whatever is the requirement
    Sandbox(const string& n) : member(n) {}//a copy is made

};

int main()
{
    Sandbox sandbox(string("four"));
    std::cout << "The answer is: " << sandbox.member << std::endl;
    return 0;
}

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