6

因此,在 c++ 中,如果将函数的返回值分配给 const 引用,则该返回值的生命周期将是该引用的范围。例如

MyClass GetMyClass()
{
    return MyClass("some constructor");
}

void OtherFunction()
{
    const MyClass& myClass = GetMyClass(); // lifetime of return value is until the end            
                                           // of scope due to magic const reference
    doStuff(myClass);
    doMoreStuff(myClass);
}//myClass is destructed

因此,似乎无论您通常将函数的返回值分配给 const 对象,您都可以将其分配给 const 引用。函数中是否存在过您不想在赋值中使用引用而是使用对象的情况?你为什么要写这行:

const MyClass myClass = GetMyClass();

编辑:我的问题让几个人感到困惑,所以我添加了 GetMyClass 函数的定义

编辑 2:如果您尚未阅读此内容,请不要尝试回答问题:http: //herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-常量/

4

6 回答 6

4

如果函数返回一个对象(而不是引用),则需要在调用函数中创建一个副本[尽管可能会采取优化步骤,这意味着该对象将直接写入最终存储的结果存储中,根据“好像”原则]。

在示例代码中const MyClass myClass = GetMyClass();,这个“复制”对象被命名myclass,而不是一个存在但没有命名的临时对象(除非你查看机器代码,否则它是可见的)。换句话说,无论你是否为它声明一个变量,MyClass函数调用中都会有一个对象GetMyClass——这只是你是否让它可见的问题。

Edit2:const参考解决方案看起来相似(不完全相同,这真的只是为了解释我的意思,你实际上不能这样做):

 MyClass __noname__ = GetMyClass();
 const MyClass &myclass = __noname__;

只是编译器__noname__在幕后生成了变量,并没有真正告诉你。

通过使const MyClass myclass对象可见并且很清楚正在发生什么(并且GetMyClass返回对象的副本,而不是对某些已经存在的对象的引用)。

另一方面,如果GetMyClass确实返回了一个引用,那么它肯定是正确的做法。

在某些编译器中,使用引用甚至可能会在使用对象时添加额外的内存读取,因为引用“是一个指针”[是的,我知道,标准没有这么说,但请在抱怨之前,做我帮个忙,给我看一个编译器,它不会将引用实现为带有额外糖的指针以使它们尝起来更甜],所以要使用引用,编译器应该读取引用值(指向对象的指针),然后读取值在该指针的对象内部。在非引用的情况下,编译器将对象本身“知道”为直接对象,而不是引用,从而节省了额外的读取。当然,大多数编译器大部分时间都会优化这种额外的引用,但它不能总是这样做。

于 2013-08-09T10:06:44.220 回答
1

一个原因是该引用可能会使您的代码的其他读者感到困惑。并不是每个人都知道对象的生命周期会扩展到引用的范围。

于 2013-08-09T09:53:27.200 回答
1

语义:

MyClass const& var = GetMyClass();

MyClass const var = GetMyClass();

非常不同。一般来说,您只会在函数本身返回引用时使用第一个(并且根据其语义要求返回引用)。而且你知道你需要注意对象的生命周期(它不在你的控制之下)。当您想要拥有(副本)该对象时,您使用第二个。在这种情况下使用第二个是误导性的,可能会导致意外(如果函数还返回对之前销毁的对象的引用)并且可能效率略低(尽管在实践中,我希望两者都生成完全相同的代码如果GetMYClass按值返回)。

于 2013-08-09T10:18:10.130 回答
0

表现

由于大多数当前编译器省略了副本(和移动),因此两个版本应该具有大致相同的效率:

const MyClass& rMyClass = GetMyClass();
const MyClass  oMyClass = GetMyClass();

在第二种情况下,语义上需要复制或移动,但可以根据 [class.copy]/31 省略它。稍有不同的是,第一个适用于不可复制的不可移动类型。

Mats Petersson 和 James Kanze 已经指出,对于某些编译器,访问引用可能会更慢。


寿命

引用应该在其整个范围内都有效,就像具有自动存储的对象一样。这个“应该”当然是由程序员强制执行的。因此,对于 IMO 读者来说,它们所暗示的寿命没有区别。虽然,如果有错误,我可能会寻找悬空引用(不信任原始代码/引用的生命周期声明)。

如果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 oMyClass;清楚地表明我拥有这个对象。考虑mutable成员:如果您稍后更改它们,读者并不清楚它是否已被声明为参考(对于所有更改)。

此外,对于引用,它所引用的对象不改变并不明显。const引用仅意味着不会更改对象,而不是没有会更改对象(*)。通过查找该变量的定义,程序员必须知道该引用是引用该对象的唯一方式。

(*) 免责声明:尽量避免不明显的副作用

于 2013-08-09T11:22:43.653 回答
0

关于实际调用的析构函数有一个主要含义。检查 Gotw88、Q3 和 A3。我把所有东西都放在一个小测试程序中(Visual-C++,所以请原谅 stdafx.h)

// 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

这里是设置的一个小解释。B 派生自 A,但两者都没有虚拟析构函数(我知道这是一个 WTF,但在这里很重要)。CreateB() 按值返回 B。Main 现在调用 CreateB 并首先将此调用的结果存储在类型 A 的 const 引用中。然后调用 CreateB 并将结果存储在类型 A 的值中。

结果很有趣。首先 - 如果您按引用存储,则调用正确的析构函数(B),如果您按值存储,则调用错误的析构函数。其次 - 如果你存储在一个引用中,析构函数只被调用一次,这意味着只有一个对象。按值导致 2 次调用(对不同的析构函数),这意味着有 2 个对象。

我的建议 - 使用 const 参考。至少在 Visual C++ 上,它可以减少复制。如果您不确定您的编译器,请使用并调整此测试程序来检查编译器。如何适应?添加复制/移动构造函数和复制赋值运算符。


我快速为 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 也一样,只需将每个大写字母 A 替换为 B)

这导致以下输出:

Reference
A constructed
B constructed
B destroyed
Value
A constructed
B constructed
A copy constructed
B destroyed
A destroyed

这证实了上面的结果(请注意,A 构造的结果来自 B 被构造为 B 是从 A 派生的,因此每当调用 Bs 构造函数时都会调用 As 构造函数)。

附加测试:Visual C++ 也接受非常量引用,其结果(在本例中)与 const 引用相同。此外,如果您使用 auto 作为类型,则(当然)会调用正确的析构函数,并且会启动返回值优化,最终它与 const 引用的结果相同(但当然,auto 的类型为 B 而不是 A) .

于 2013-08-09T12:06:30.237 回答
0

我不明白你想要达到什么目的。T const&可以(在堆栈上)绑定到从函数返回的(按值)的原因T是使其他函数可以将此临时作为T const&参数。这可以防止您要求创建重载。但是无论如何都必须构造返回值。

但是今天(使用 C++11)你可以使用const auto myClass = 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();
  • var1用以下结果初始化version_a()
  • var2用返回的引用version_b()所属的对象的副本初始化
  • var3持有对返回的临时对象的 const 引用并延长其生命周期
  • var4使用从返回的引用进行初始化version_b()
  • var5如同var1
  • var6如同var4

它们在语义上都是不同的。var3出于我上面给出的原因而工作。只有var5var6自动存储返回的内容。

于 2013-08-09T10:36:42.790 回答