问题 什么时候应该从类方法返回对象的引用


从类方法返回引用的最佳实践是什么?是否需要在没有引用的情况下返回基本类型,而您希望通过引用返回的类对象。您推荐的任何文章,最佳实践文章。


11294
2018-01-30 07:09


起源



答案:


我假设通过类方法你的意思是成员函数。而“通过引用返回”表示“返回对成员数据的引用”。这主要是为了返回对本地的引用,这显然是错误的。

什么时候应该返回对成员数据的引用,何时返回数据本身?

默认情况下,您应该返回数据本身(也就是“按值”)。这避免了返回引用时的几个问题:

  • 用户存储引用并变得依赖于 一生 您的成员,不考虑包含对象(您的对象)将存在多长时间。导致悬空指针。

  • 用户代码依赖于确切的 返回类型。例如,您使用a vector<T> 实现(这是你的getter返回的)。用户代码如“vector<T> foo = obj.getItems()“出现。然后你改变你的实现(和getter)来使用a deque<T>  - 用户代码中断。如果您按值返回,则只需使getter创建一个本地向量,从成员deque中复制数据,然后返回结果。小型收藏品非常合理。 [*]

那么什么时候应该返回参考?

  • 当返回的对象很大时你可以考虑它(Image)或不可复制的(boost::signal)。但是,与往常一样,您可以选择更多的OOP模式来拥有您的课程  东西而不是东西 挂着它。在里面 Image 你可以提供一个 drawCircle 成员函数,而不是返回 Image& 并让您的用户在其上绘制一个圆圈。
  • 当您的数据在逻辑上由您的用户拥有时,您只是为他持有它。考虑std集合: vector<T>::operator[] 返回一个 参考 因为这就是我想要的:我的确切对象,而不是它的副本。

[*]有一种更好的方法可以确保面向未来的代码。而不是返回一个向量(通过ref by value)返回一对迭代器到你的向量 - 一个开始和一个结束。这可以让您的用户这样做 一切 它们通常使用双端队列或向量,但与实际实现无关。提升提供 boost::iterator_pair 以此目的。作为一个特权,它也有运算符[]重载,所以你甚至可以做到“int i = obj.getItems()[5]“ 而不是 ”int i = obj.getItems().begin()[5]”。

此解决方案适用于任何允许您一般处理类型的情况。例如,如果你保持一个 Dog 会员,但您的用户只需知道它是一个 Animal (因为他们只打电话 eat() 和 sleep()),返回一个 动物 引用/指向您的狗的freestore分配副本的指针。然后当你决定狗是弱者并且你真的需要一只狼来实现时,用户代码不会破坏。

这种 信息隐藏 不仅仅是确保未来的兼容性。它还有助于保持您的设计清洁。


6
2018-01-30 07:38



我曾经认为隐藏类型是有用的,但随着时间的推移,这虽然以某种方式逐渐消失。一世 想 调用代码来了解确切的类型。在更改实现时,有时我会故意重命名函数本身,作为实际查看所有调用代码的方法。因人而异。 - NeARAZ


答案:


我假设通过类方法你的意思是成员函数。而“通过引用返回”表示“返回对成员数据的引用”。这主要是为了返回对本地的引用,这显然是错误的。

什么时候应该返回对成员数据的引用,何时返回数据本身?

默认情况下,您应该返回数据本身(也就是“按值”)。这避免了返回引用时的几个问题:

  • 用户存储引用并变得依赖于 一生 您的成员,不考虑包含对象(您的对象)将存在多长时间。导致悬空指针。

  • 用户代码依赖于确切的 返回类型。例如,您使用a vector<T> 实现(这是你的getter返回的)。用户代码如“vector<T> foo = obj.getItems()“出现。然后你改变你的实现(和getter)来使用a deque<T>  - 用户代码中断。如果您按值返回,则只需使getter创建一个本地向量,从成员deque中复制数据,然后返回结果。小型收藏品非常合理。 [*]

那么什么时候应该返回参考?

  • 当返回的对象很大时你可以考虑它(Image)或不可复制的(boost::signal)。但是,与往常一样,您可以选择更多的OOP模式来拥有您的课程  东西而不是东西 挂着它。在里面 Image 你可以提供一个 drawCircle 成员函数,而不是返回 Image& 并让您的用户在其上绘制一个圆圈。
  • 当您的数据在逻辑上由您的用户拥有时,您只是为他持有它。考虑std集合: vector<T>::operator[] 返回一个 参考 因为这就是我想要的:我的确切对象,而不是它的副本。

[*]有一种更好的方法可以确保面向未来的代码。而不是返回一个向量(通过ref by value)返回一对迭代器到你的向量 - 一个开始和一个结束。这可以让您的用户这样做 一切 它们通常使用双端队列或向量,但与实际实现无关。提升提供 boost::iterator_pair 以此目的。作为一个特权,它也有运算符[]重载,所以你甚至可以做到“int i = obj.getItems()[5]“ 而不是 ”int i = obj.getItems().begin()[5]”。

此解决方案适用于任何允许您一般处理类型的情况。例如,如果你保持一个 Dog 会员,但您的用户只需知道它是一个 Animal (因为他们只打电话 eat() 和 sleep()),返回一个 动物 引用/指向您的狗的freestore分配副本的指针。然后当你决定狗是弱者并且你真的需要一只狼来实现时,用户代码不会破坏。

这种 信息隐藏 不仅仅是确保未来的兼容性。它还有助于保持您的设计清洁。


6
2018-01-30 07:38



我曾经认为隐藏类型是有用的,但随着时间的推移,这虽然以某种方式逐渐消失。一世 想 调用代码来了解确切的类型。在更改实现时,有时我会故意重命名函数本身,作为实际查看所有调用代码的方法。因人而异。 - NeARAZ


重载赋值运算符(如=,+ =, - =等)是一个很好的例子,通过引用返回很有意义。这种方法显然会返回大对象,并且您不想返回指针,因此返回引用是最好的方法。像指针一样工作,看起来像按值返回。


4
2018-01-30 07:19



有效C ++中的Meyers详细解释。 - Calyth


引用是伪装的指针(因此它在32位机器上是4个字节,在64位机器上是8个字节)。所以经验法则是:如果复制对象比返回指针更昂贵,请使用指针(或引用,因为这是相同的事情)。

复制哪些类型更昂贵取决于体系结构,编译器,类型本身等。在某些情况下复制一个16字节的对象 能够 比返回指针要快(例如,如果对象映射到SSE寄存器或类似情况)。

当然,现在返回对局部变量的引用是没有意义的。因为函数退出后局部变量将消失。所以通常你会返回成员变量,全局/静态变量或动态分配对象的引用/指针。

有些情况下你  想要返回对象的指针/引用,即使复制对象很昂贵。大多数情况下,当您不想将调用代码绑定到原始对象的生命周期中时。


2
2018-01-30 07:15





斯科特迈耶斯的书, 有效的C ++,有几个与此主题相关的项目。我肯定会看看标题为“你不要试图回复参考”的项目 必须 返回一个对象。“这是第1版或第2版中的第23项,或第3版中的#21。


1
2018-01-30 15:04



你的意思是第21项。 - Larry Engholm
避免 有人在互联网上出错了,我已经更新了我(差不多三岁)的答案。 - Michael Kristofik


我建议远离返回引用,原因与Iraimbilanja指出的相同,但在我看来,通过对成员数据使用共享指针(例如,boost,tr1)可以获得非常好的结果并使用它们作为回报。这样您就不必复制对象,但仍可以管理生命周期问题。

class Foo
{
private:
    shared_ptr<Bar> _bar;
public:
    shared_ptr<Bar> getBar() {return _bar;}
};

通常,复制Bar的成本大于构建新的shared_ptrs的成本,如果不是这样的话,它仍然值得用于终身管理。


1
2018-01-30 08:13





如果 NULL 是一个可能的返回值,该方法必须返回一个指针,因为您无法返回引用 NULL


0
2018-05-08 04:49





按值返回基本类型,除非您要让调用者访问实际成员。

通过引用返回类对象(甚至是std :: string)。


-1
2018-01-30 08:43