我应该使用shared_ptr还是unique_ptr?

54

我正在使用pimpl方法创建一些对象,但我不确定是否应该使用std::shared_ptr还是std::unique_ptr

我知道std::unique_ptr更高效,但对我来说这并不是很重要,因为这些对象本身就比较庞大,所以std::shared_ptr相对于std::unique_ptr的成本相对较小。

我目前选择使用std::shared_ptr只是因为它更加灵活。例如,使用std::shared_ptr允许我将这些对象存储在哈希表中以便快速访问,同时仍然能够将这些对象的副本返回给调用者(因为我认为任何迭代器或引用可能会很快失效)。

然而,这些对象在某种程度上并没有被复制,因为更改会影响所有副本,因此我想知道,也许使用std::shared_ptr并允许复制是某种反模式或不好的事情。

这样正确吗?


1
使用其中之一会深刻改变你给对象的拷贝语义。两者都有用途。我认为在C++世界中更通用的是 unique_ptr,但是共享实现的对象也有它们的用处,特别是如果你正在编写“外部”代码(例如COM、C++/CLI),或者如果这个类真的看起来像是一个“引用类型”。 - Alexandre C.
类似问题:https://dev59.com/YnVC5IYBdhLWcg3wYQEp - Rolf Kristensen
1
在C++11中推荐的方法是使用unique_ptr,毕竟你不需要将实现进行复制或共享。此外,unique_ptr在运行时更快。 - Damian
4个回答

38
我一直在使用pimpl习惯,但我不确定是使用shared_ptr还是unique_ptr。 绝对应该使用unique_ptr或scoped_ptr。 Pimpl不是一个模式,而是一个习惯用法,它处理编译时依赖性和二进制兼容性。它不应影响对象的语义,特别是与其复制行为有关的方面。 您可以在内部使用任何类型的智能指针,但这两个保证您不会意外共享两个不同对象之间的实现,因为它们需要有意识地决定复制构造函数和赋值运算符的实现。 然而,这些对象在某种程度上并没有被复制,因为更改会影响所有副本,所以我想也许使用shared_ptr并允许复制是某种反模式或坏事情。 这不是一个反模式,实际上,这是一种模式:别名。您已经在C++中使用裸指针和引用。shared_ptr提供了额外的“安全”措施,以避免死引用,但代价是额外的复杂性和新问题(注意创建内存泄漏的循环)。

与 Pimpl 无关

我知道 unique_ptr 更高效,但对我来说这不是太大的问题,因为这些对象本身就相对较重,所以 shared_ptr 相对于 unique_ptr 的成本相对较小。

如果你可以分离出一些状态,你可能需要看一下 Flyweight 模式。


3
@Clinton:从语义上讲,拷贝与其来源分离,实现细节对用户来说并不重要。在C++中,指针和引用并非如此,因此在讨论这些方面时会出现笨拙的情况。无论您希望为类选择深拷贝还是浅拷贝,都应该只取决于您想要给类赋予的语义,实现将随之而来。 - Matthieu M.
1
@Frank:COW是一种选择(我确实说过shared_ptr适用于某些情况),但我们在这里讨论的是Pimpl;这不是那些适用shared_ptr的情况之一。 - Matthieu M.
@conio:CRTP 不仅适用于 C 或 C++,我认为它至少可以在 D 中使用,可能还可以在 Java/C# 中使用(不确定,因为它似乎需要无限制泛型)。无论如何,这些术语肯定存在重叠,并且其他人对它们的含义有不同的看法,... 这有点混乱 :x - Matthieu M.
@MatthieuM。同样适用于D语言的是pimpl。它并不特定于C++。请参阅https://dlang.org/spec/struct.html和https://dev59.com/nnrZa4cB1Zd3GeqPxglc。 - conio
@Ben:我不会用std::any来做这个,使用 basic_any 可能因为它的内联存储方式而变得有趣,或者真正地使用 std::aligned_storage_t - Matthieu M.
显示剩余11条评论

12
如果你使用 shared_ptr,它并不是经典的 pimpl idiom(除非你采取额外措施)。但真正的问题在于为什么要使用智能指针;很明显,在哪里调用 delete 是非常清楚的,并且没有异常安全或其他需要考虑的问题。最多,智能指针会帮你省略一两行代码。而唯一具有正确语义的只有 boost::scoped_ptr,但我认为它在这种情况下不起作用。(如果我没记错的话,它需要一个完整的类型才能实例化。) pimpl idiom 的重要方面是其使用应对客户端透明;该类应该像经典实现一样运作。这意味着要么禁止复制和赋值,要么实现深度复制,除非该类是不可变的(没有非 const 成员函数)。通常的智能指针都不实现深度复制;当然,你可以实现一个深度复制,但每当发生复制时,它可能仍然需要一个完整的类型,这意味着你仍然必须提供一个用户定义的复制构造函数和赋值运算符(因为它们不能是内联的)。鉴于此,使用智能指针可能不值得麻烦。
一个例外是如果对象是不可变的。在这种情况下,无论复制是深度还是浅度的都没有关系,shared_ptr 完全处理了这种情况。

6
当你使用一个shared_ptr(例如在容器中,然后通过值查找并返回它),你并不是在导致所指向对象的副本,而仅仅是指针的副本与引用计数。这意味着如果你从多个点修改底层对象,则会影响同一实例。这正是设计它的目的,所以不是某种反模式!当传递shared_ptr时(如注释所述),最好通过const引用传递,并在需要时复制(因此增加引用计数)。至于返回情况,则要具体问题具体分析。

2
这是很好的建议,但我认为“始终通过值传递和返回”有点过于强硬。首选是通过const引用或rvalue引用传递。甚至有时仍然适合通过普通可修改的引用传递。 - Potatoswatter
@Johann:啊,我在思考按值传递对具有成员(pimpl)shared_ptrunique_ptr的对象产生的影响。在这种情况下,智能指针本身不应该被传递。 - Potatoswatter
2
@Johann Gerell:不,你不应该通过值传递shared_ptr。虽然可以这样做,但这不是推荐的方法。推荐的方法是通过const引用传递它们。这比复制shared_ptr更有效,并且绝对安全,因为根据shared_ptr文档,同一shared_ptr实例的并发读取是可以的。 - antred

0

是的,请使用它们。简单来说,shared_ptr 是智能指针的一种实现。unique_ptr 是自动指针的一种实现:


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