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

Returning a const reference to an object instead of a copy

本文关键字:副本 const 引用 对象 返回      更新时间:2023-10-16

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

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

当然,getter会更好地返回const std::string&? 当前方法是返回效率不高的副本。 返回常量引用会导致任何问题吗?

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

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

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

编辑:现代C++(即C++11及更高版本)支持返回值优化,因此不再不赞成按值返回内容。 仍然应该注意按值返回非常大的对象,但在大多数情况下应该没问题。

实际上,另一个专门返回字符串而不是通过引用的问题是,std::string通过c_str()方法通过指向内部const char*的指针提供访问。 这让我头疼地调试了好几个小时。 例如,假设我想从 foo 获取名称,并将其传递给 JNI 以用于构造一个稍后传递给 Java 的 jstring,并且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.

如果你的调用者要做这种事情,最好使用某种类型的智能指针或常量引用,或者至少在你的 foo.name()方法上有一个讨厌的警告注释标题。 我之所以提到 JNI,是因为以前的 Java 程序员可能特别容易受到这种类型的方法链的影响,否则这种方法链可能看起来无害。

const 引用返回的一个问题是,如果用户编码如下内容:

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

通过std::string返回,临时对象将保持活动状态并附加到 str,直到 str 超出范围。

但是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 引用返回(因为,无论如何,我只是更愿意发送引用而不是希望编译器会优化额外的临时),只要遵守以下约定:"如果你想要它超出我的对象存在,他们会在我的对象被破坏之前复制它"

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

如果您担心性能,请对其进行基准测试(<=不能强调这一点)!!尝试这两种方法并测量增益(或缺乏增益)。如果一个更好,你真的很在乎,那就使用它。如果没有,那么它提供的保护更适合其他人提到的终身问题。

你知道他们怎么说做出假设...

好的

,所以返回副本和返回引用之间的区别是:

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

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

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

对其进行操作)。

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

如果您有多个线程调用 name(),则会出现一个潜在的皱纹。 如果返回引用,但稍后更改基础值,则调用方的值将更改。 但是现有的代码看起来无论如何都不是线程安全的。

看看迪玛对一个相关潜在但不太可能的问题的回答。

可以

想象,如果调用者真的想要副本,您可能会破坏某些内容,因为他们即将更改原件并希望保留它的副本。然而,它更有可能的是,它实际上应该只返回一个常量引用。

最简单的方法是尝试它,然后测试它以查看它是否仍然有效,前提是您可以运行某种测试。如果没有,我会先专注于编写测试,然后再继续重构。

如果您更改为常量引用,该函数的典型用法不会中断的可能性非常好。

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

有关系吗?一旦使用现代优化编译器,按值返回的函数将不涉及副本,除非语义上需要。

请参阅有关此内容的 C++ lite 常见问题解答。

取决于你需要做什么。 也许您希望所有调用方在不更改类的情况下更改返回值。 如果您返回不会飞行的常量引用。

当然,下一个论点是调用者可以制作自己的副本。 但是,如果您知道如何使用该函数并且知道无论如何都会发生这种情况,那么这样做可能会在代码中节省您稍后的步骤。

我通常会返回const&,除非我不能。QBziZ给出了一个例子来说明这种情况。当然,QBziZ也声称std::string具有写入时复制语义,这在今天很少见,因为COW在多线程环境中涉及大量开销。通过返回 const 和你把责任放在调用者身上,用他们那端的字符串做正确的事情。但是,由于您正在处理已在使用的代码,因此您可能不应该更改它,除非分析显示此字符串的复制会导致大量性能问题。然后,如果您决定更改它,则需要粗略地进行测试以确保您没有破坏任何东西。希望与您合作的其他开发人员不要像 Dima 的答案那样做粗略的事情。

返回对成员的引用会公开该类的实现。这可能会阻止改变类。如果需要优化,可能对私有或受保护的方法有用。C++吸气者应该返回什么