114

假设我有一个类,其方法返回一个shared_ptr.

按引用或按值返回它可能有哪些好处和缺点?

两个可能的线索:

  • 早期对象破坏。如果我返回shared_ptrby (const) 引用,则引用计数器不会增加,因此当对象超出另一个上下文(例如另一个线程)的范围时,我会承担删除对象的风险。这个对吗?如果环境是单线程的,会不会也出现这种情况呢?
  • 成本。价值传递当然不是免费的。是否值得尽可能避免它?

谢谢大家。

4

2 回答 2

134

按值返回智能指针。

正如您所说,如果您通过引用返回它,您将无法正确增加引用计数,这会带来在不适当的时间删除某些内容的风险。仅此一项就足以成为不通过引用返回的理由。接口应该是健壮的。

由于返回值优化(RVO),成本问题现在已经没有实际意义,因此您不会在现代编译器中产生增量 - 增量 - 减量序列或类似的东西。所以返回 a 的最好方法shared_ptr是简单地按值返回:

shared_ptr<T> Foo()
{
    return shared_ptr<T>(/* acquire something */);
};

对于现代 C++ 编译器来说,这是一个非常明显的 RVO 机会。我知道即使关闭所有优化,Visual C++ 编译器也会实现 RVO。而使用 C++11 的移动语义,这种担忧就更不重要了。(但唯一可以确定的方法是分析和实验。)

如果您仍然不相信,Dave Abrahams 有一篇文章提出了按价值返回的论点。我在这里复制了一个片段;我强烈建议您阅读整篇文章:

老实说:下面的代码让你感觉如何?

std::vector<std::string> get_names();
...
std::vector<std::string> const names = get_names();

坦率地说,即使我应该知道得更多,这让我感到紧张。原则上,当get_names() 返回时,我们必须复制svector的a string。然后,我们需要在初始化时再次复制它 names,并且我们需要销毁第一个副本。如果向量中有 Nstring个,则每个副本可能需要多达 N+1 个内存分配和大量缓存不友好的数据访问 > 随着字符串内容的复制。

我没有面对这种焦虑,而是经常依靠传递引用来避免不必要的复制:

get_names(std::vector<std::string>& out_param );
...
std::vector<std::string> names;
get_names( names );

不幸的是,这种方法远非理想。

  • 代码增长了 150%
  • 我们不得不放弃const-ness 因为我们正在改变名称。
  • 正如函数式程序员喜欢提醒我们的那样,变异通过破坏引用透明性和等式推理,使代码的推理变得更加复杂。
  • 我们不再有名称的严格值语义。

但是真的有必要这样搞乱我们的代码来提高效率吗?幸运的是,答案是否定的(尤其是在使用 C++0x 时)。

于 2012-05-17T21:13:08.193 回答
25

关于任何智能指针(不仅仅是 shared_ptr),我认为返回一个引用是不可接受的,而且我会非常犹豫是否通过引用或原始指针传递它们。为什么?因为你不能确定它以后不会通过引用被浅拷贝。您的第一点定义了为什么这应该是一个问题的原因。即使在单线程环境中也可能发生这种情况。您不需要并发访问数据即可将错误的复制语义放入您的程序中。一旦将指针传递出去,您就无法真正控制用户对指针的操作,因此不要鼓励滥用,给您的 API 用户足够的绳索让自己上吊。

其次,如果可能的话,看看你的智能指针的实现。建造和破坏应该几乎可以忽略不计。如果这个开销是不可接受的,那么不要使用智能指针!但除此之外,您还需要检查您所拥有的并发架构,因为对跟踪指针使用的机制的互斥访问会比仅仅构建 shared_ptr 对象更慢。

编辑,3 年后:随着 C++ 中更现代的特性的出现,我会调整我的答案以更容易接受当你简单地编写一个永远不会存在于调用函数范围之外的 lambda 的情况,而不是复制到别的地方。在这里,如果您想节省复制共享指针的最小开销,那将是公平和安全的。为什么?因为你可以保证引用永远不会被误用。

于 2012-05-17T21:17:42.997 回答