我们应该按引用还是按值传递shared_ptr?


269

当一个函数接受一个shared_ptr(来自boost或C ++ 11 STL)时,是否要传递它:

  • 通过const参考: void foo(const shared_ptr<T>& p)

  • 或值:void foo(shared_ptr<T> p)

我更喜欢第一种方法,因为我怀疑它会更快。但这真的值得吗?还是有其他问题?

请问您提供选择的理由,或者如果是这种情况,请说明原因。


14
问题是那些不相等。参考版本尖叫“我要给一些别名shared_ptr,如果需要我可以更改它。”,而值版本则说“我要复制您的shared_ptr,因此尽管我可以更改它,但您永远不会知道。 )const-reference参数才是真正的解决方案,它表示“我要为某些别名shared_ptr,并且保证不会更改。”(这与按值语义非常相似!)
GManNickG 2010年

2
嘿,我有兴趣的人的意见对返回shared_ptr类成员。你通过const-refs做到吗?
Johannes Schaub-litb

第三种可能性是将std :: move()与C ++ 0x一起使用,这会交换两个shared_ptr
Tomaka17

@Johannes:我将通过const-reference返回它,以避免任何复制/引用计数。再一次,除非它们是原始的,否则我将通过const-reference返回所有成员。
GManNickG

Answers:


228

斯科特,安德烈(Andrei)和赫伯(Herb)在C ++及以后的2011年的《任何问题》会议上讨论并回答了这个问题。从4:34 开始观看性能和正确性shared_ptr

不久,没有理由按值传递,除非目标是共享对象的所有权(例如,在不同的数据结构之间或在不同的线程之间)。

除非您可以按照上面链接的视频中Scott Meyers的说明进行移动优化,但这与您可以使用的C ++的实际版本有关。

GoingNative 2012会议的“ 交互式面板:任何问题!”中对该讨论进行了重大更新值得一看,尤其是从22:50开始


5
但如此处所示,按值传递比较便宜:同样不应考虑stackoverflow.com/a/12002668/128384(至少对于构造函数参数,例如,其中shared_ptr将成为其成员)班上)?
stijn

2
@stijn是和否。您指出的问题与解答是不完整的,除非它阐明了所引用的C ++标准的版本。散布容易误导的一般永不/总是规则非常容易。除非,否则读者会花一些时间来熟悉David Abrahams的文章和参考,或者将发布日期与当前C ++标准进行比较。因此,给定发布时间,我的答案和您指出的答案都是正确的。
mloskot

1
除非有多线程 ”否则,MT绝非特别。
curiousguy

3
我参加聚会太晚了,但是我想按值传递shared_ptr的原因是它使代码更短,更漂亮。说真的 Value*简短易读,但是很糟糕,所以现在我的代码已经写满了const shared_ptr<Value>&,可读性也大大降低了,整洁了。void Function(Value* v1, Value* v2, Value* v3)现在过去是什么void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3),人们对此表示满意吗?
亚历克斯

7
@Alex常见的做法是在课程之后立即创建别名(typedefs)。以您的示例为例:class Value {...}; using ValuePtr = std::shared_ptr<Value>;然后您的函数变得更简单:void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)并且您将获得最佳性能。这就是使用C ++的原因,不是吗?:)
4LegsDrivenCat

91

这是赫伯·萨特的

准则:除非您要使用或操纵智能指针本身(例如共享或转让所有权),否则请勿将智能指针作为函数参数传递。

指导原则:表示函数将使用按值shared_ptr参数存储和共享堆对象的所有权。

准则:仅使用非常量shared_ptr&参数来修改shared_ptr。仅在不确定是否要复制并共享所有权时,才使用const shared_ptr&作为参数。否则,请改用widget *(或者,如果不能为null,则使用widget&)。


3
感谢您与萨特的链接。这是一篇很棒的文章。我不同意他在widget *上的看法,如果C ++ 14可用,则更喜欢可选的<widget&>。widget *与旧代码过于模糊。
2015年

3
+1包括将widget *和widget&包括在内。只是为了详细说明,当函数不检查/修改指针对象本身时,传递widget *或widget&可能是最佳选择。接口更通用,因为它不需要特定的指针类型,并且可以避免shared_ptr参考计数的性能问题。
tgnottingham

4
由于第二条准则,我认为这应该是当今公认的答案。显然,它使当前接受的答案无效,即:没有理由按值传递。
mbrt '18

63

我个人将使用const参考。无需为了函数调用而再次递减参考计数。


1
我没有否决您的答案,但是在您偏爱此事之前,要考虑的两种可能性各有利弊。知道和讨论这些利弊是一件好事。之后,每个人都可以自己做决定。
Danvil

@Danvil:考虑到shared_ptr工作原理,未通过引用的唯一可能缺点是性能略有下降。这里有两个原因。a)指针别名功能意味着要复制指向指针的数据和一个计数器(对于弱引用可能为2),因此复制数据要稍微贵一点。b)原子引用计数比普通的旧增量/减量代码慢一点,但是为了确保线程安全,这是必需的。除此之外,两种方法在大多数意图和目的上都是相同的。
埃文·特兰


22

我跑到下面的代码,一旦与foo服用shared_ptrconst&,并再次fooshared_ptr的值。

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

在我的Intel Core 2 Quad(2.4GHz)处理器上使用VS2015,x86版本构建

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

按值复制版本要慢一个数量级。
如果要从当前线程同步调用函数,请选择该const&版本。


1
您能否说说您使用了什么编译器,平台和优化设置?
卡尔顿

我使用了vs2015的调试版本,现在更新了答案以使用发布版本。
tcb 2015年

1
我很好奇,如果启用优化功能后,两者的结果相同
Elliot Woods

2
优化没有太大帮助。问题是副本上引用计数上的锁争用。
亚历克斯(Alex)

1
那不是重点。这样的foo()函数甚至根本不应该接受共享指针,因为它没有使用该对象:它应该接受a int&和do p = ++x;foo(*p);从调用main()。函数需要在需要执行某操作时接受一个智能指针对象,并且在大多数情况下,您需要将其移动std::move()到其他地方,因此按值参数没有成本。
eepp

14

从C ++ 11开始,您应该比您想像的更多地重视const&上的价值

如果您使用的是std :: shared_ptr(而不是基础类型T),那么您这样做是因为您想对此做一些事情。

如果您想将其复制到某处,则更有意义的是将其复制并std ::内部移动,而不是通过const&进行复制然后再复制。这是因为在调用函数时,允许调用者选项依次std :: move shared_ptr,从而为自己节省了一组递增和递减操作。或不。也就是说,函数的调用者可以在调用函数之后并根据是否移动来决定是否需要std :: shared_ptr。如果您通过const&,这是无法实现的,因此最好按值取值。

当然,如果调用者都需要他的shared_ptr更长的时间(因此无法std :: move),并且您不想在函数中创建纯副本(例如,您想要一个弱指针,或者您有时只想复制它(取决于某些条件),那么const&可能仍然是首选。

例如,您应该

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

过度

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

因为在这种情况下,您总是在内部创建副本


1

我不知道其中原子增量和减量所在的shared_copy复制操作的时间成本,我遭受了更高的CPU使用率问题。我没想到原子的增加和减少会花费那么多的成本。

根据我的测试结果,int32原子增量和减量是非原子增量和减量的2或40倍。我在装有Windows 8.1的3GHz Core i7上获得了它。前一个结果是在没有争用发生时出现的,后者是在发生争用的可能性很高时出现的。我要记住,原子操作最终是基于硬件的锁定。锁就是锁。发生争用时对性能不利。

遇到这种情况,我总是使用byref(const shared_ptr&)而不是byval(shared_ptr)。


0

最近有一篇博客文章:https : //medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

因此,答案是:不要(几乎)永不过去const shared_ptr<T>&
只需传递基础类即可。

基本上,唯一合理的参数类型是:

  • shared_ptr<T> -修改并取得所有权
  • shared_ptr<const T> -不要修改,要拥有所有权
  • T& -修改,无所有权
  • const T& -不要修改,没有所有权
  • T -请勿修改,没有所有权,可以廉价复制

正如@accel在https://stackoverflow.com/a/26197326/1930508中指出的那样,来自Herb Sutter的建议是:

仅在不确定是否要复制并共享所有权时,才使用const shared_ptr&作为参数

但是,您不确定有多少种情况?所以这是一种罕见的情况


0

众所周知的问题是,按值传递shared_ptr会产生成本,应尽可能避免。

shared_ptr传递的费用

大多数情况下,通过引用传递shared_ptr,甚至通过const引用传递更好。

cpp核心准则有一个用于传递shared_ptr的特定规则

R.34:采取shared_ptr参数表示函数是部件所有者

void share(shared_ptr<widget>);            // share -- "will" retain refcount

确实有必要在值上传递shared_ptr的一个示例是,当调用方将共享对象传递给异步被调用方时-即,在被调用方完成其工作之前,调用方超出范围。被调用者必须通过按值获取share_ptr来“延长”共享对象的寿命。在这种情况下,不会传递对shared_ptr的引用。

将共享库传递到工作线程也是如此。


-4

shared_ptr不够大,其构造函数\析构函数也没有做足够的工作以使副本有足够的开销来关心按引用传递与按副本传递的性能。


15
你测量了吗?
curiousguy

2
@stonemetal:创建新的shared_ptr期间原子指令如何处理?
Quarra 2014年

它是非POD类型,因此在大多数ABI中,甚至“按值”传递它实际上也会传递一个指针。根本不是字节的实际复制。如您在asm输出中看到的那样,传递一个shared_ptr<int>by值会占用100多个x86指令(包括昂贵的locked指令,以原子方式增加/减少引用计数)。传递常量ref与传递指向任何对象的指针相同(在此示例中,在Godbolt编译器资源管理器上,尾部调用优化将其转换为简单的jmp而不是调用:godbolt.org/g/TazMBU)。
彼得·科德斯

TL:DR:这是C ++,在其中复制构造函数可以完成的工作不仅仅是复制字节。这个答案就是垃圾。
彼得·科德斯

2
stackoverflow.com/questions/3628081/shared-ptr-horrible-speed作为示例,通过值传递的共享指针与通过引用传递的共享指针的运行时差大约为33%。如果您正在处理对性能至关重要的代码,那么裸指针可以为您带来更大的性能提升。因此,如果您记得的话,一定要通过const ref传递,但是如果您不记得的话,这没什么大不了的。如果不需要,不要使用shared_ptr更为重要。
stonemetal
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.