为什么不总是将返回值赋给const引用?

时间:2016-06-06 03:42:12

标签: c++

我们说我有一些功能:

Foo GetFoo(..)
{
  ...
}

假设我们既不知道这个函数是如何实现的,也不知道Foo的内部结构(例如,它可能是非常复杂的对象)。但是我们知道函数正在按值返回Foo,并且我们希望将此返回值用作const。

问题:将此函数的返回值存储为const &始终是一个好主意吗?

const Foo& f = GetFoo(...);

代替,

const Foo f = GetFoo(...);

我知道编译器会返回值优化,可能会移动对象而不是复制它,所以最后const &可能没有任何优势。不过我的问题是,有没有缺点?为什么我不应该开发肌肉记忆始终使用const &来存储返回值,因为我不必依赖编译器优化以及甚至移动的事实对于复杂的物体,操作可能很昂贵。

将此扩展到极端,为什么我总是使用const &来代替我的代码中不可变的所有变量?例如,

const int& a = 2;
const int& b = 2;
const int& c = c + d;

除了更冗长,还有什么缺点吗?

4 个答案:

答案 0 :(得分:26)

这些有语义上的差异,如果你要求的东西不是你想要的,如果你得到它就会遇到麻烦。考虑this code

#include <stdio.h>

class Bar
{
    public:
    Bar() { printf ("Bar::Bar\n"); }
    ~Bar() { printf ("Bar::~Bar\n"); }
    Bar(const Bar&) { printf("Bar::Bar(const Bar&)\n"); }
    void baz() const { printf("Bar::Baz\n"); }
};

class Foo
{
    Bar bar;

    public:
    Bar& getBar () { return bar; }
    Foo() { }
};

int main()
{
    printf("This is safe:\n");
    {
        Foo *x = new Foo();
        const Bar y = x->getBar();
        delete x;
        y.baz();
    }
    printf("\nThis is a disaster:\n");
    {
        Foo *x = new Foo();
        const Bar& y = x->getBar();
        delete x;
        y.baz();
    }
    return 0;
}

输出是:

  

这是安全的:
  酒吧::酒吧
  Bar :: Bar(const Bar&amp;)
  酒吧::〜酒吧
  酒吧::巴兹
  Bar :: ~Bar

     

这是一场灾难:
  酒吧::酒吧
  酒吧::〜酒吧
  Bar :: Baz

请注意,我们在Bar::Baz被销毁后致电Bar。糟糕。

询问你想要什么,如果你得到你所要求的,你就不会被搞砸。

答案 1 :(得分:26)

致电elision&#34;优化&#34;是一种误解。允许编译器不这样做,但也允许它们实现a+b整数加法作为一系列按位操作和手动进位。

执行该操作的编译器会产生敌意:编译器也是如此拒绝消失。

Elision不喜欢&#34;其他&#34;优化,因为那些依赖于as-if规则(行为可能会改变,只要它的行为如果标准规定)。 Elision可能会改变代码的行为。

至于为什么使用const &或甚至右值&&是一个坏主意,引用是对象的别名。使用其中任何一个,您都没有(本地)保证对象不会在其他地方被操纵。实际上,如果函数返回&const&&&,则该对象必须在其他位置存在,并且在实践中具有另一个标识。所以你的本地&#34;值是对某个未知的遥远状态的引用:这使得本地行为难以推理。

另一方面,值不能别名。您可以在创建后形成此类别名,但无法在标准下修改const本地值,即使存在别名也是如此。

关于本地对象的推理很容易。关于分布式对象的推理很难。引用按类型分布:如果您在引用或值的情况下进行选择,并且该值没有明显的性能成本,始终选择值。

具体:

Foo const& f = GetFoo();

可以是对Foo类型的临时值的引用绑定,也可以是从GetFoo()返回的派生,GetFoo()中存储的其他内容绑定的引用。我们无法从这条线上说出来。

Foo const& GetFoo();

VS

Foo GetFoo();

make f具有不同的含义,实际上。

Foo f = GetFoo();

始终会创建副本。没有什么不能修改&#34;通过&#34; f将修改f(当然,除非它的ctor将指针传递给其他人)。

如果我们有

const Foo f = GetFoo();

我们甚至可以保证修改(mutable部分f部分是未定义的行为。我们可以假设f是不可变的,实际上编译器会这样做。

const Foo&案例中,如果基础存储不是f ,则可以定义行为来修改const。所以我们不能假设f是不可变的,并且编译器只会假设它是不可变的,如果它可以检查所有具有有效派生指针或对f的引用的代码并确定它们都没有变异(即使您只是传递const Foo&,如果原始对象是非const Foo,则对const_cast<Foo&>合法并修改它。

简而言之,不要过早悲观并假设省略并且不会发生&#34;不会发生&#34;。很少有当前的编译器在没有明确关闭的情况下不会失败,而且你几乎肯定不会在他们身上构建一个严肃的项目。

答案 2 :(得分:13)

基于@David Schwartz在评论中所说的内容,您需要确保语义不会改变。你打算将这个值视为不可变的是不够的,你得到它的功能应该把它视为不可变的,否则你会得到一个惊喜。

image.SetPixel(x, y, white_pixel);
const Pixel &pix = image.GetPixel(x, y);
image.SetPixel(x, y, black_pixel);
cout << pix;

答案 3 :(得分:3)

在考虑的情况下(选择变量的类型时)const C&const C之间的语义差异可能会影响您在下面列出的案例中的程序。它们不仅必须在编写新代码时考虑,还应在后续维护期间考虑,因为源代码的某些更改可能会在变量定义属于此分类的位置发生变化。

初始化程序是一个完全类型为C

的左值
const C& foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1)引入了一个独立的对象(在类型C的复制语义允许的范围内),而(2)创建了另一个对象的别名,并受到该对象发生的所有更改(包括其寿命结束。)

初始值设定项是从C

派生的类型的左值
struct D : C { ... };
const D& foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1)是foo()返回的切片版本。 (2)绑定到派生对象并且可以享受多态行为(如果有的话)的好处,但是有被别名问题困扰的风险。

初始值设定项是从C

派生的类型的右值
struct D : C { ... };
D foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

对于(1),这与前一种情况没有什么不同。关于(2),没有更多的混淆!常量引用绑定到派生类型的临时类型,其生命周期延伸到封闭范围的末尾,并自动调用正确的析构函数(~D())。 (2)可以享受多态性带来的好处,但要支付DC相比所消耗的额外资源的价格。

初始化程序是可转换为类型C

的左值的类型的右值
struct B {
    C c;
    operator const C& () const { return c; }
};
const B foo();
const C  a = foo(); // (1)
const C& b = foo(); // (2)

(1)复制并继续,而(2)在下一个语句中立即开始时遇到麻烦,因为它将死对象的子对象别名化了!