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

Shi*_*hah 34 c++

假设我有一些功能:

Foo GetFoo(..)
{
  ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

const Foo& f = GetFoo(...);
Run Code Online (Sandbox Code Playgroud)

代替,

const Foo f = GetFoo(...);
Run Code Online (Sandbox Code Playgroud)

我知道编译器会返回值优化,可能会移动对象而不是复制它,所以最终const &可能没有任何优势.不过我的问题是,有什么缺点吗?为什么不应该只发展肌肉的内存总是使用const &存储因为我不必依赖于编译器优化和事实,即使移动操作可以为复杂的对象昂贵的返回值.

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

const int& a = 2;
const int& b = 2;
const int& c = c + d;
Run Code Online (Sandbox Code Playgroud)

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

Yak*_*ont 27

将elision称为"优化"是一种误解.允许编译器不这样做,但也允许编译器实现a+b整数加法作为一系列按位操作和手动进位.

执行此操作的编译器将是恶意的:编译器也拒绝消失.

Elision不像"其他"优化,因为那些依赖于as-if规则(行为可能会改变,只要它的行为如果标准规定).Elision可能会改变代码的行为.

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

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

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

具体来说:

Foo const& f = GetFoo();
Run Code Online (Sandbox Code Playgroud)

既可以是参考结合到临时类型的Foo或衍生自返回GetFoo(),结合到别的存储内的参考GetFoo().我们无法从这条线上说出来.

Foo const& GetFoo();
Run Code Online (Sandbox Code Playgroud)

VS

Foo GetFoo();
Run Code Online (Sandbox Code Playgroud)

使f具有不同的含义,有效.

Foo f = GetFoo();
Run Code Online (Sandbox Code Playgroud)

总是创建一个副本.没有任何不修改"通过"的东西f会修改f(当然,除非它的ctor将指针传递给其他人).

如果我们有

const Foo f = GetFoo();
Run Code Online (Sandbox Code Playgroud)

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

在这种const Foo&情况下,如果底层存储是非存储,f则可以定义修改行为.因此我们不能假设它是不可变的,并且编译器只会认为它是不可变的,如果它可以检查所有具有有效派生的指针或引用的代码并确定它们都没有变异它(即使你只是传递,如果是原始的对象是非Foo,它是合法的并且可以修改它).constffconst Foo&constconst_cast<Foo&>

总之,不要过早悲观并假设省略"不会发生".很少有当前的编译器在没有明确关闭的情况下不会丢失,而且你几乎肯定不会在它们上构建一个严肃的项目.


Dav*_*rtz 26

这些都有语义上的差异,如果你要求的东西不是你想要的,那么如果你得到它就会遇到麻烦.考虑以下代码:

#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;
}
Run Code Online (Sandbox Code Playgroud)

输出是:

这是安全的:
Bar :: Bar
Bar :: Bar(const Bar&)
Bar ::〜Bar
Bar :: Baz
Bar :: ~Bar

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

请注意我们Bar::BazBar被销毁后打电话.哎呀.

问你想要什么,这样你就不会被搞砸了.

  • 这似乎是在回答与最初提出的问题不同的问题。这描述了当您定义返回引用(或不返回)的函数时会发生什么。问题是关于_使用_函数并将其返回值分配给引用(或不分配)。 (3认同)
  • @Zaibis:问题是引用,而不是`const`. (2认同)
  • @DavidSchwartz - 问题的例子是该方法通过 _value_ 返回。您的示例方法通过引用返回,因此您的第二个示例将是一场灾难。如果 Foo::getBar 按值返回,则您的第二个示例不会是灾难。(因此,这就是为什么我说你没有回答最初的问题。) (2认同)

Mar*_*som 13

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

image.SetPixel(x, y, white_pixel);
const Pixel &pix = image.GetPixel(x, y);
image.SetPixel(x, y, black_pixel);
cout << pix;
Run Code Online (Sandbox Code Playgroud)