当我不知道C++返回类型是否为临时类型时,应该使用什么返回类型?

12

假设 Foo 是一个相当大的数据结构。如果我不知道继承类是否会在内部存储 Foo 的实例,那么我应该如何编写一个返回 Foo 实例的 const 虚函数?如果无法在内部存储它,我的理解是无法返回一个 const 引用,因为它将是一个临时变量。这正确吗?两个选项是:

virtual Foo foo() const { ... }
virtual Foo const & foo() const { ... }

这里有一个相关问题,但是从不同的角度来看。


2
从你抓住多少人答案措手不及的情况来看,这是一个很好的问题。当设计类接口时,我也被迫考虑了这个问题。我没有想出特别好的解决方案,但还没有花时间自问这个问题。+1 - Cody Gray
3
http://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/ - user195488
@0A0D 我认为Herb Sutter的文章并不相关,因为在他的情况下,他通过值返回了一个对象,然后引用它。而在这种情况下,问题是是否通过引用或值返回。尽管如此,这仍然是一篇很棒的文章! - Bjarke H. Roune
4个回答

9

您对值返回和const引用返回之间的区别感兴趣,仅仅是出于优化的考虑,但实际上不是这样的。每次返回不同的值与每次返回引用有根本上不同的含义,可能是对同一对象的返回引用,该对象很可能被修改:

const Foo &a = myobj.foo();
myobj.modify_the_foo();
const Foo &b = myobj.foo();
a == b; // do you want this to be true or false?

调用者需要知道它是哪一个,因为程序员需要知道它的含义,编译器需要知道调用约定,所以你不能在同一虚函数的不同重载中混合使用它们。如果一些派生类想要做一件事,而另一些想要做另一件事,那就太糟糕了,他们不能这样做,就像一个函数返回 int,另一个返回 float 一样。
也许你可以返回一个 shared_ptr。这样,“想要”返回引用的派生类可以创建一个带有什么都不做的删除器的 shared_ptr(但要注意——如果原始对象被销毁,shared_ptr 将悬空,这不是你通常从返回的 shared_ptr 中期望的)。因此,如果 Foo 要比它来自的对象存在更长的时间,那么最好的方法是通过动态分配来保存它,通过 shared_ptr 持有它,并返回该副本,而不是一个什么都不做的删除器。 "想要"返回值的派生类可以每次分配一个新值。由于 Foo 是“相当大”的,希望 shared_ptr 和动态分配的成本与您为创建要返回的新值所做的任何操作相比不会太高。
另一个可能性是将 Foo 转换为一个小的 pImpl 风格类,该类引用一个相当大的数据结构。如果所涉及的所有内容都是不可变的,那么“想要返回引用”的情况可以在多个 Foo 实例之间共享大型数据结构。即使它不是这样,你也可以考虑写时复制。

3

我看到你没有将C++0x列为一个标签,但是对于任何有需要并且可以访问C++0x的人来说,也许最好的方法是返回一个std::unique_ptr<>


2
返回翻译后的文本:或者...在没有C++0x的情况下,boost::unique_ptr可以发挥奇效。 - sehe
1
@sehe:绝对没错!我的错 - 我现在太沉迷于C++0x了,以至于我甚至忘记了曾经有一段时间boost给过我同样的服务! - Johann Gerell
Boost没有提供unique_ptr类型,因为这是不可能的,除非使用C++11,在那里已经有了标准的unique_ptr。在C++03中,如果您想返回一个没有引用计数的智能指针,则可以使用auto_ptr - Dennis Zickefoose
@Dennis: 虽然不是Boost的官方部分,但它与之紧密集成:http://home.roadrunner.com/~hinnant/unique_ptr03.html,由Howard Hinnant提供。 - Johann Gerell

1
这是一种方法:
struct K 
 { 
 int ii; 
 };

class I 
  { 
  virtual K &get_k(int i)=0; 
  };

class Impl : public I 
  { 
  K &get_k(int i) { kk.ii = i; return kk; } 
  K kk; 
  };

它的工作原理在于您在同一对象中使用K kk; 作为数据成员。Impl类的构造函数也可能很有用。

编辑:更改代码格式


3
这里没有代码行限制。将其分成多行使其更易读。对我来说,整个类定义在一行上读起来像一团糟。 - Cody Gray

1

如果您不知道派生类是否可以存储对象,则不能通过引用返回。因此,对于您的问题的严格答案是必须通过值返回。

其他答案建议返回指针或智能指针,这也可以工作。但是,那么不存储对象的客户端将不得不执行动态分配,这可能比移动或复制甚至是相当大的对象更慢。

如果您的主要关注点是避免复制,则可以通过使接口稍微不那么好看来实现:

virtual void foo(Foo& putReturnvalueHere) const { ... }

将之前要返回的值分配给传入的引用。这需要Foo已经被构造。如果这不可接受,您可以传递一个指向未构造内存区域的指针,该区域将保存一个Foo,然后使用放置new来在该内存区域中构造一个Foo:

virtual void foo(Foo* unconstructedFoo) const { ... }

我不建议最后这个想法,除非你知道自己在做什么并且确实需要最高的性能。如果性能如此重要,你可能需要考虑避免首先进行虚函数调用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接