C++ 悬空引用(不信任原始代码/引用的生存期声明)
如果C++ 悬空引用(不信任原始代码/引用的生存期声明),c++,C++,如果GetMyClass可以(合理地)更改以返回引用,则必须确保该对象的生存期足够,例如 SomeClass* p = /* ... */; void some_function(const MyClass& a) { /* much code with many side-effects */ delete p; a.do_something(); // oops! } const MyClass& r = p->get_reference(
GetMyClass
可以(合理地)更改以返回引用,则必须确保该对象的生存期足够,例如
SomeClass* p = /* ... */;
void some_function(const MyClass& a)
{
/* much code with many side-effects */
delete p;
a.do_something(); // oops!
}
const MyClass& r = p->get_reference();
some_function(r);
所有权 一个直接命名对象的变量,如
const MyClass代码>明确说明我拥有此对象。考虑<代码>易变的成员:如果您稍后更改它们,则如果读者已被声明为引用,对于读者而言,这是不确定的(对于所有的更改)。
此外,对于一个引用,它引用的对象是否没有改变并不明显。const
引用仅表示您不会更改对象,而不是没有人会更改对象(*)。程序员必须知道,通过查找该变量的定义,该引用是引用该对象的唯一方式
(*)免责声明:尽量避免不明显的副作用关于实际调用的析构函数有一个重要的含义。检查Gotw88、Q3和A3。我把所有东西都放在一个小测试程序中(Visual C++,所以请原谅stdafx.h)
这里有一个关于设置的小说明。B是从A派生的,但两者都没有虚拟析构函数(我知道这是一个WTF,但在这里它很重要)。CreateB()按值返回B。Main现在调用CreateB,并首先将此调用的结果存储在类型a的const引用中。然后调用CreateB,并将结果存储在类型a的值中
结果很有趣。首先-如果按引用存储,则调用正确的析构函数(B),如果按值存储,则调用错误的析构函数。第二,如果存储在引用中,析构函数只调用一次,这意味着只有一个对象。By值导致2个调用(对不同的析构函数),这意味着有2个对象
我的建议-使用const参考。至少在VisualC++中,它导致拷贝更少。如果您对编译器不确定,请使用并调整此测试程序来检查编译器。如何适应?添加复制/移动构造函数和复制赋值运算符
我很快为A类和B类添加了复制和赋值运算符
A(const A& rhs)
{
std::cout<<"A copy constructed"<<std::endl;
}
A& operator=(const A& rhs)
{
std::cout<<"A copy assigned"<<std::endl;
}
这证实了上面的结果(请注意,从B构造为B的A构造结果是从A派生的,因此每当调用Bs构造函数时,都会调用as构造函数)
附加测试:VisualC++将非同一引用(同样在这个例子中)作为const引用接受。此外,如果您使用auto作为类型,则会调用正确的析构函数(当然)并进行返回值优化,最终结果与const引用相同(当然,auto具有类型B而不是A)。
我不知道您为什么要这样做?除此之外,返回的值可能不是引用,因此仍有一个副本正在生成[即使该副本是由编译器生成的用于保存返回对象的未命名临时对象]。。。如果没有看到GetMyClass()
的返回类型,我无法确定。如果函数有可能按值返回,则不希望使用引用,因为它具有误导性。如果函数保证返回一个引用,那么选择是使用返回值所引用的对象(包括考虑它的生存期可能比本地引用短),还是使用自己的对象,具有定义的生存期。@JamesKanze在我的示例中,它确实按值返回。@dan在这种情况下,没有理由使用引用。@dan和我已经阅读了您提到的文章,但它似乎没有说任何我们都不知道的东西。它没有说明为什么有人会想做那样的事情;如果是这样的话,它就不得不说没有理由,这只是糟糕的编程实践。或者一些读者可能会认为GetMyClass()通过引用而不是通过值(例如,对于单例)返回现有变量,并且此局部对象上的更改可能会影响其他对象,这些对象将在代码中的其他地方创建(稍后或在其他线程中)。即使引用是常量,在内部状态或并发运行时问题中也可能存在一些可变值。@user1837009根据该参数,我们应该到处按值传递,而不是按常量引用传递。我认为没有人会这么认为!@user1837009可能更切题:当使用由f返回的引用时函数,您(和编译器)必须考虑到对象可能会在其他地方的代码中更改其值。@JamesKanze但由于函数是按值返回的,这是代码中对该对象的唯一引用。因此其他任何东西都不能修改它。@dan但编译器必须对此进行推断。(我指的是Mats的文章,他在文章中说,大多数编译器都将引用作为隐藏的指针来实现。)如果对象返回一个对象而不是一个引用,为什么需要创建一个副本?该对象最终会出现在哪里?我说的副本是到接收函数中的临时存储区-它是一个“未命名对象”或命名对象[从技术上讲,编译器有时会制作两个副本,但返回值优化允许编译器将其制作成一个副本]。该副本很可能根据[class.copy]删除/31.但是,它需要是可复制的。如果省略了副本,那么它将在函数本身中完成。在OtherFunction
中必须有MyClass
对象的存储-可能是GetMyClass
函数直接写入编译器作为优化传递的存储中。对象还会在哪里存储?Herb Sutter简单地说,复制的对象将一直保留,直到const
引用超出范围。但是字符串对象存在
MyClass __noname__ = GetMyClass();
const MyClass &myclass = __noname__;
MyClass const& var = GetMyClass();
MyClass const var = GetMyClass();
MyClass version_a();
MyClass const& version_b();
const MyClass var1 =version_a();
const MyClass var2 =version_b();
const MyClass var3&=version_a();
const MyClass var4&=version_b();
const auto var5 =version_a();
const auto var6 =version_b();
const MyClass& rMyClass = GetMyClass();
const MyClass oMyClass = GetMyClass();
SomeClass* p = /* ... */;
void some_function(const MyClass& a)
{
/* much code with many side-effects */
delete p;
a.do_something(); // oops!
}
const MyClass& r = p->get_reference();
some_function(r);
// Gotw88.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
class A
{
protected:
bool m_destroyed;
public:
A() : m_destroyed(false) {}
~A()
{
if (!m_destroyed)
{
std::cout<<"A destroyed"<<std::endl;
m_destroyed=true;
}
}
};
class B : public A
{
public:
~B()
{
if (!m_destroyed)
{
std::cout<<"B destroyed"<<std::endl;
m_destroyed=true;
}
}
};
B CreateB()
{
return B();
}
int _tmain(int argc, _TCHAR* argv[])
{
std::cout<<"Reference"<<std::endl;
{
const A& tmpRef = CreateB();
}
std::cout<<"Value"<<std::endl;
{
A tmpVal = CreateB();
}
return 0;
}
Reference
B destroyed
Value
B destroyed
A destroyed
A(const A& rhs)
{
std::cout<<"A copy constructed"<<std::endl;
}
A& operator=(const A& rhs)
{
std::cout<<"A copy assigned"<<std::endl;
}
Reference
A constructed
B constructed
B destroyed
Value
A constructed
B constructed
A copy constructed
B destroyed
A destroyed