返回对对象的const引用,而不是副本


73

在重构某些代码时,我遇到了一些返回std :: string的getter方法。例如:

class foo
{
private:
    std::string name_;
public:
    std::string name()
    {
        return name_;
    }
};

当然,吸气剂返回一个更好const std::string&?当前方法返回的副本效率不高。返回const引用会引起任何问题吗?


谁说这没效率。在std :: string上已经做了很多工作,以使此操作高效。传递参考和strincg之间存在差异,但是实际差异通常不明显。
马丁·约克

11
@Loki:没有人说它会有效,事实上,最新版本的C ++可能会。使该代码合理有效的唯一优化将是引用计数的std :: string。但是,许多STL实现并没有进行引用计数,因为在现代多核CPU上,管理引用计数的速度比您想象的要慢。因此,是的,返回副本要慢得多。多年来,GCC和Microsoft的STL都没有依靠std :: string进行引用。
肖恩·米德迪奇

3
@seanmiddleditch:在您假设之前,请务必进行测量(这就是我要说的)。编译器还可以应用许多其他优化:内联和复制省略是将成本降低到零的两种方法。
马丁·约克

4
@Loki:是的,这是个好建议;始终配置文件。同样好的建议是知道“假设”和“事实”之间的区别。我向您保证,在符合C ++编译器的任何实现中,Rob的代码都永远不会导致零开销。该语言要求从foo :: name()返回时,至少必须调用一次复制构造函数,即使使用内联(对何时调用构造函数也没有影响),复制省略(不适用于此代码)和RVO(可能会减少多余的复制构造函数调用,但不会消除所有的调用)。
肖恩·米德迪奇

3
@Loki:我确实尝试在GCC 4.6和VC 10上都进行了测量,同时都启用了最大优化功能,并得到了相同的结果:调用了复制构造函数。即使考虑了as-if规则,这也是“显而易见的”结果,因为std :: string的copy构造函数实际上确实具有副作用:它要么创建一个全新的字符数组和该字符串的副本,要么对其进行操作参考计数值(取决于实现)。请注意,在引用计数的impl上做一个足够简单的微型基准测试可能不会在这里给您“真实”的结果,我在第一次尝试时就在GCC上遇到过。
肖恩·米德迪奇

Answers:


58

可能导致问题的唯一方法是,调用方是否存储引用,而不是复制字符串,并在对象被销毁后尝试使用它。像这样:

foo *pFoo = new foo;
const std::string &myName = pFoo->getName();
delete pFoo;
cout << myName;  // error! dangling reference

但是,由于您现有的函数返回一个副本,因此您不会破坏任何现有代码。

编辑:现代C ++(即C ++ 11及更高版本)支持返回值优化,因此不再按值返回事物。仍然应该记住按值返回非常大的对象,但是在大多数情况下应该可以。


不会破坏现有代码,但不会获得任何性能优势。如果像示例中一样容易发现问题,那很好,但是通常要棘手得多。例如,如果您有vector <foo>和push_back,导致大小调整等。优化等)
tfinniga

7
@Dima:在这种情况下,大多数编译器可能会执行命名值返回值优化。
理查德·科登

1
@Richard:是的,我最近读过。但这仅适用于您将返回值立即分配给某些对象的情况,对吗?如果返回值传递给函数怎么办?临时设施的建设是否还会优化?
Dima

1
那么最终判决是什么?最好将参考传递回去或仅传递副本?让我们假设编译器是愚蠢的,并且程序员必须像在我们变得懒惰的前一天那样,必须是具体而周到的:)
Volte

2
@戴尔,我建议返回一个const引用。
迪马2012年

32

实际上,另一个专门针对通过引用返回字符串的问题是,通过c_str()方法std::string提供了通过指向内部的指针的访问的事实。这使我花了很多小时来调试。例如,假设我想从foo中获取名称,并将其传递给JNI,以用于构造一个jstring以便稍后传递给Java,这将返回一个副本而不是一个引用。我可能会这样写:const char*name()

foo myFoo = getFoo(); // Get the foo from somewhere.
const char* fooCName = foo.name().c_str(); // Woops!  foo.name() creates a temporary that's destructed as soon as this line executes!
jniEnv->NewStringUTF(fooCName);  // No good, fooCName was released when the temporary was deleted.

如果您的调用者将要执行此类操作,则最好使用某种类型的智能指针或const引用,或者至少在foo.name()方法上使用讨厌的警告注释标头。我之所以提到JNI,是因为以前的Java编码人员可能特别容易受到这种类型的方法链接的攻击,而这些方法看起来似乎无害。


3
该死的,好捕获。尽管我认为C ++ 11指定了临时对象的寿命,以使此代码对任何兼容的编译器均有效。
马克·麦肯纳

@MarkMcKenna很高兴知道。我做了一些搜索,这个回答似乎表明即使使用C ++ 11,您仍然需要稍微小心一点:stackoverflow.com/a/2507225/13140
Ogre Psalm33

18

const引用返回的一个问题是用户是否编码如下:

const std::string & str = myObject.getSomeString() ;

有了std::string回报,直到海峡进入的范围之临时对象将仍然活着,并连接到海峡。

但是使用a会发生什么const std::string &?我的猜测是,我们将有一个const引用到一个对象,该对象在其父对象解除分配时可能会死亡:

MyObject * myObject = new MyObject("My String") ;
const std::string & str = myObject->getSomeString() ;
delete myObject ;
// Use str... which references a destroyed object.

因此,我偏爱const引用返回(因为无论如何,我只希望发送一个引用而不是希望编译器优化额外的临时文件),只要遵守以下约定:我的物体的存在,他们会在我的物体销毁之前复制它”


10

std :: string的某些实现使用写时复制语义共享内存,因此按值返回几乎可以与按引用返回一样高效,并且您不必担心生命周期问题(运行时确实给你)。

如果您担心性能,请对其进行基准测试(<=不能足够强调)!!!尝试两种方法并测量增益(或增益不足)。如果一个更好,并且您真的在乎,请使用它。如果不是这样,则更喜欢按值来提供保护,从而提供其他人提到的终生问题。

你知道他们说些什么...


相反,在修改不相关的字符串时,您必须担心线程安全:)
bk1e

10
嘿。运行时所提供的东西,将多线程处理掉了。:)
rlerallut

1
出于好奇-g ++是否使用写时复制语义实现std :: string?
法兰克

4
@ user60628 COW不受欢迎(十年前),请参阅本文
Motti

实际上,C ++ 11和更高版本不允许使用COW。
MM

7

好的,因此返回副本和返回引用之间的区别是:

  • 性能:返回参考可能更快,也可能不会更快;它取决于std::string编译器实现的实现方式(正如其他人指出的那样)。但是,即使您返回引用,函数调用后的赋值通常也包含一个副本,例如std::string name = obj.name();

  • 安全:返回参考可能会或可能不会导致问题(悬挂参考)。如果函数的用户不知道自己在做什么,将引用存储为引用,并在提供对象超出范围后使用它,那么就存在问题。

如果您想要快速安全地使用boost :: shared_ptr。您的对象可以在内部将字符串存储为,shared_ptr并返回shared_ptr。这样,就不会复制要复制的对象,而且它始终是安全的(除非您的用户get()在对象超出范围后拉出原始指针并对其进行处理)。


4

我将其更改为返回const std :: string&。如果您不更改所有调用代码,则调用者可能仍会复制结果,但不会带来任何问题。

如果您有多个调用name()的线程,则可能会引起皱纹。如果返回引用,但随后又更改了基础值,则调用者的值将更改。但是现有代码无论如何看起来都不是线程安全的。

看看迪玛(Dima)对于一个相关的潜在问题的答案,但这不太可能。


3

可以想象,如果呼叫者真的想要一份副本,您可能会破坏某些内容,因为他们将要更改原始文件并希望保留其副本。但是,实际上,它更有可能只是返回一个const引用。

最简单的方法是尝试一下,然后对其进行测试,以查看它是否仍然有效,前提是您可以进行某种测试。如果不是这样,那么我将着重于先编写测试,然后再继续进行重构。


1

很有可能,如果您更改为const引用,则该函数的典型用法不会中断。

如果所有调用该函数的代码都在您的控制之下,则只需进行更改,然后查看编译器是否抱怨。


1

有关系吗?使用现代优化的编译器后,按值返回的函数将不包含副本,除非语义上需要它们。

有关此信息,请参见C ++ lite常见问题解答


值返回优化是否如此复杂,以至于它会使用指向副本原始成员值io的指针/引用?我认为情况并非如此。RBV优化跳过了返回值的几个步骤,但是尽管就地版本,它仍将调用cstring的副本ctor。
QBziZ

如果函数如本例中那样返回类成员变量的副本,则它实际上将返回copy。原始成员无法在此过程中销毁(与按值返回并确实创建要返回的临时函数不同)
MM

0

取决于您需要做什么。也许您希望所有调用者都更改返回的值而不更改类。如果返回的const引用将不会生效。

当然,下一个论点是调用者可以随后制作自己的副本。但是,如果您知道该函数将如何使用并且无论如何都会发生,那么也许这样做可以为您节省代码的后一步。


我认为这不是问题。调用者可以执行“ std :: string s = aFoo.name()”,而s将是可变的副本。
克里斯托弗·约翰逊

0

我通常会返回const&,除非我不能这样做。QBziZ给出了这种情况的示例。当然,QBziZ还声称std :: string具有写时复制的语义,这种情况在当今很少见,因为在多线程环境中COW涉及很多开销。通过返回const&,您可以将调用者的责任放在调用者身上,以使字符串结尾处的内容正确无误。但是,由于您正在处理已在使用的代码,因此除非概要分析表明复制此字符串会导致严重的性能问题,否则您可能不应更改它。然后,如果您决定进行更改,则需要进行彻底的测试以确保您没有损坏任何东西。希望与您合作的其他开发人员不要像Dima的回答那样做一些粗略的事情。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.