当const引用生命周期是当前范围的长度时,为什么使用const非引用

时间:2013-08-09 09:31:20

标签: c++

因此,在c ++中,如果将函数的返回值赋给const引用,那么该返回值的生命周期将是该引用的范围。 E.g。

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

6 个答案:

答案 0 :(得分:4)

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

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

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

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

只是编译器在幕后生成__noname__变量,而没有实际告诉你。

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

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

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

答案 1 :(得分:1)

一个原因是该引用可能会使您的代码的其他读者感到困惑。并非所有人都意识到对象的生命周期延伸到参考范围。

答案 2 :(得分:1)

语义:

MyClass const& var = GetMyClass();

MyClass const var = GetMyClass();

非常不同。一般来说,你只会使用 首先,当函数本身返回一个引用时(并且是 需要通过其语义返回引用)。你呢 知道你需要注意的一生 对象(不受你的控制)。你使用第二个 当你想拥有(副本)对象时。使用第二个 在这种情况下是误导,可能会导致意外(如果 function也返回对象的引用 早先被破坏了)并且可能效率稍差 (虽然在实践中,我希望两者都能完全生成 如果GetMYClass按值返回,则使用相同的代码。)

答案 3 :(得分:0)

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

但是今天(使用C ++ 11)你可以使用T const&

修改 作为可能发生的事情的一个例子,我将介绍一些东西:

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()包含对返回的temoprary的const引用并延长其生命周期
  • 使用var3 返回的引用初始化
  • var4
  • version_b()var5
  • 相同
  • var1var6
  • 相同

他们的语义完全不同。 var4因我上面给出的原因而起作用。只有var3var5会自动存储返回的内容。

答案 4 :(得分:0)

效果

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

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

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

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


寿命

引用应该在整个范围内有效,就像具有自动存储的对象一样。这个“ should ”当然是由程序员强制执行的。因此,对于读者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引用仅暗示不会更改对象,而 nobody 不会更改对象(*)。程序员必须知道这个引用是引用该对象的唯一方法,通过查找该变量的定义。

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

答案 5 :(得分: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 ++上,它可以减少复制。如果您不确定编译器,请使用并调整此测试程序以检查编译器。怎么适应?添加复制/移动构造函数和复制赋值运算符。


我很快添加了副本&amp; A级和A级的分配操作员乙

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)

这导致以下输出:

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

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

其他测试:Visual C ++也接受非const引用,其结果与本例相同(在本例中)。另外,如果你使用auto作为类型,调用正确的析构函数(当然)并且返回值优化开始,最后它与const引用的结果相同(当然,auto有B类而不是A)