有哪些 C++ 智能指针实现可用?

Posted

技术标签:

【中文标题】有哪些 C++ 智能指针实现可用?【英文标题】:What C++ Smart Pointer Implementations are available? 【发布时间】:2011-02-17 07:29:54 【问题描述】:

比较、优点、缺点以及何时使用?

这是 garbage collection thread 的衍生产品,我认为这是一个简单的答案,产生了很多关于某些特定智能指针实现的 cmets,因此似乎值得开始一篇新文章。

最终的问题是 C++ 中智能指针的各种实现是什么,它们如何比较?只是简单的利弊或例外,以及您可能认为应该有效的一些问题。

我已经发布了一些我已经使用或至少掩饰并考虑用作下面的答案的实现,以及我对它们的差异和相似之处的理解可能不是 100% 准确,因此请随时进行事实检查或更正我根据需要。

目标是了解一些新的对象和库,或者纠正我对已经广泛使用的现有实现的使用和理解,并最终为其他人提供一个体面的参考。

【问题讨论】:

我认为这应该作为这个问题的答案重新发布,并将问题变成一个实际问题。否则,我觉得人们会将此视为“不是一个真正的问题”。 还有各种各样的智能指针,例如the ATL smart pointers 或 OpenSceneGraph's osg::ref_ptr. 这里有问题吗? 我认为你误解了std::auto_ptrstd::auto_ptr_refstd::auto_ptr 的设计细节。 std::auto_ptr 与垃圾回收无关,它的主要目的是专门允许异常安全地转移所有权,尤其是在函数调用和函数返回的情况下。 std::unique_ptr 只能解决您在标准容器中引用的“问题”,因为 C++ 已更改为允许区分移动和复制,而标准容器已更改为利用这一点。 您说您不是智能指针方面的专家,但您的总结非常详尽且正确(除了关于 auto_ptr_ref 是实现细节的小问题)。不过,我同意您应该将其发布为 answer 并将问题重新表述为实际问题。这可以作为未来的参考。 【参考方案1】:

C++03

std::auto_ptr - 也许是它遭受初稿综合症的原件之一,仅提供有限的垃圾收集设施。第一个缺点是它在销毁时调用delete,这使得它们不能用于保存数组分配的对象(new[])。它拥有指针的所有权,因此两个自动指针不应包含相同的对象。赋值将转移所有权并将 rvalue 自动指针重置为空指针。这可能导致最严重的缺点;由于上述无法复制,它们不能在 STL 容器中使用。对任何用例的最后打击是它们将在下一个 C++ 标准中被弃用。

std::auto_ptr_ref - 这不是智能指针,它实际上是与std::auto_ptr 结合使用的设计细节,以允许在某些情况下进行复制和分配。具体来说,它可用于使用 Colvin-Gibbons 技巧(也称为 move constructor)将非 const std::auto_ptr 转换为 左值 以转移所有权。

相反,std::auto_ptr 可能并不是真正打算用作自动垃圾回收的通用智能指针。我的大部分有限理解和假设都基于Herb Sutter's Effective Use of auto_ptr,我确实经常使用它,尽管并不总是以最优化的方式。


C++11

std::unique_ptr - 这是我们的朋友,他将替换 std::auto_ptr 它将非常相似,除了用于纠正 std::auto_ptr 的弱点的关键改进,例如使用数组,lvalue 保护通过私有复制构造函数,可用于 STL 容器和算法等。由于它的性能开销和内存占用是有限的,因此它是替换或更恰当地描述为拥有原始指针的理想候选者。正如“唯一”所暗示的那样,指针只有一个所有者,就像之前的 std::auto_ptr 一样。

std::shared_ptr - 我相信这是基于 TR1 和 boost::shared_ptr 但经过改进以包括别名和指针算法。简而言之,它将引用计数的智能指针包装在动态分配的对象周围。由于“共享”意味着当最后一个共享指针的最后一个引用超出范围时,指针可以由多个共享指针拥有,那么该对象将被适当地删除。这些也是线程安全的,并且在大多数情况下可以处理不完整的类型。 std::make_shared 可用于使用默认分配器有效地构造具有一个堆分配的std::shared_ptr

std::weak_ptr - 同样基于 TR1 和 boost::weak_ptr。这是对 std::shared_ptr 拥有的对象的引用,因此如果 std::shared_ptr 引用计数降至零,则不会阻止删除该对象。为了访问原始指针,您首先需要通过调用lock 访问std::shared_ptr,如果拥有的指针已过期并已被销毁,它将返回一个空的std::shared_ptr。这主要用于避免在使用多个智能指针时无限期挂起引用计数。


提升

boost::shared_ptr - 在最多样化的场景(STL、PIMPL、RAII 等)中可能最容易使用,这是一个共享引用计数智能指针。在某些情况下,我听到了一些关于性能和开销的抱怨,但我一定忽略了它们,因为我不记得争论是什么了。显然,它已经流行到足以成为一个待定的标准 C++ 对象,并且没有想到智能指针方面的规范的缺点。

boost::weak_ptr - 与之前对std::weak_ptr 的描述非常相似,基于此实现,这允许对boost::shared_ptr 的非拥有引用。毫不奇怪,您调用lock() 来访问“强”共享指针并且必须检查以确保它是有效的,因为它可能已经被销毁了。只需确保不要存储返回的共享指针,并在完成后立即让它超出范围,否则您将立即回到循环引用问题,您的引用计数将挂起并且对象不会被破坏。

boost::scoped_ptr - 这是一个简单的智能指针类,开销很小,可能设计用于在可用时更好地替代boost::shared_ptr。它与 std::auto_ptr 相当,尤其是它不能安全地用作 STL 容器的元素或与指向同一对象的多个指针一起使用。

boost::intrusive_ptr - 我从未使用过它,但据我了解,它旨在用于创建自己的智能指针兼容类。您需要自己实现引用计数,如果您希望您的类是通用的,您还需要实现一些方法,此外您还必须实现自己的线程安全。从好的方面来说,这可能为您提供了最自定义的方式来准确选择您想要多少或多少“智能”。 intrusive_ptr 通常比shared_ptr 更有效,因为它允许您为每个对象分配一个堆。 (感谢 Arvid)

boost::shared_array - 这是数组的boost::shared_ptr。基本上new []operator[],当然还有delete []。这可以在STL容器中使用,据我所知,boost:shared_ptr可以做所有事情,尽管你不能使用boost::weak_ptr。但是,您也可以使用boost::shared_ptr<std::vector<>> 来获得类似的功能,并重新获得使用boost::weak_ptr 进行引用的能力。

boost::scoped_array - 这是数组的boost::scoped_ptr。与boost::shared_array 一样,所有必要的数组都包含在内。这个是不可复制的,因此不能在STL 容器中使用。我发现几乎任何你想使用它的地方你都可以使用std::vector。我从来没有确定哪个实际上更快或开销更少,但这个范围数组似乎比 STL 向量少得多。如果您想在堆栈上保持分配,请考虑使用boost::array


Qt

QPointer - 在 Qt 4.0 中引入,这是一个“弱”智能指针,仅适用于 QObject 和派生类,在 Qt 框架中几乎一切,所以这并不是真正的局限性。但是存在一些限制,即它不提供“强”指针,尽管您可以使用isNull() 检查底层对象是否有效,但您可能会发现您的对象在通过检查后立即被销毁,尤其是在多线程环境中.我相信 Qt 人们认为这已被弃用。

QSharedDataPointer - 这是一个可能与boost::intrusive_ptr 相媲美的“强”智能指针,尽管它具有一些内置的线程安全性,但它确实需要您包含引用计数方法(refderef)可以通过子类化QSharedData 来实现。与大部分 Qt 一样,对象最好通过充分的继承和子类化来使用,这似乎是预期的设计。

QExplicitlySharedDataPointer - 与QSharedDataPointer 非常相似,只是它不隐式调用detach()。我将这个版本称为QSharedDataPointer 的 2.0 版,因为在引用计数降至零后准确何时分离的控制方面的轻微增加并不特别值得一个全新的对象。

QSharedPointer - 原子引用计数、线程安全、可共享指针、自定义删除(数组支持),听起来就像智能指针应该具备的一切。这是我在 Qt 中主要用作智能指针的东西,我发现它与 boost:shared_ptr 相当,尽管可能像 Qt 中的许多对象一样开销更大。

QWeakPointer - 你感觉到重复出现的模式了吗?就像std::weak_ptrboost::weak_ptr 一样,当您需要两个智能指针之间的引用时,它与QSharedPointer 一起使用,否则会导致您的对象永远不会被删除。

QScopedPointer - 这个名字也应该看起来很熟悉,实际上它是基于boost::scoped_ptr,这与 Qt 版本的共享指针和弱指针不同。它的功能是提供单个所有者智能指针,而无需 QSharedPointer 的开销,这使其更适合兼容性、异常安全代码以及您可能使用 std::auto_ptrboost::scoped_ptr 的所有内容。

【讨论】:

我认为有两件事值得一提:intrusive_ptr 通常比shared_ptr 更高效,因为它允许您为每个对象分配一个堆。 shared_ptr 通常会为引用计数器分配一个单独的小堆对象。 std::make_shared 可用于两全其美。 shared_ptr 只需一个堆分配。 我有一个可能不相关的问题:是否可以通过将所有指针替换为shared_ptrs 来实现垃圾回收? (不包括解析循环引用) @Seth Carnegie:并不是所有的指针都会指向在免费存储上分配的东西。 @the_mandrill 但是,如果拥有类的析构函数是在与客户端代码不同的翻译单元(.cpp 文件)中定义的,则它可以工作,无论如何在 Pimpl-idiom 中都给出了客户端代码。因为这个翻译单元通常知道 Pimpl 的完整定义,因此它的析构函数(当它破坏 auto_ptr 时)正确地破坏了 Pimpl。当我看到这些警告时,我也对此感到担心,但我尝试了它并且它有效(调用了 Pimpl 的析构函数)。 PS.:请使用@-syntax 让我看到任何回复。 通过添加适当的文档链接增加了列表的实用性。【参考方案2】:

还有Loki,它实现了基于策略的智能指针。

关于基于策略的智能指针的其他参考,解决了许多编译器对空基优化以及多重继承的支持不佳的问题:

Smart Pointers Reloaded A Proposal to Add a Policy-Based Smart Pointer Framework to the Standard Library

【讨论】:

【参考方案3】:

除了给出的,还有一些安全导向的:

SaferCPlusPlus

mse::TRefCountingPointer 是一个引用计数智能指针,如std::shared_ptr。不同之处在于mse::TRefCountingPointer 更安全、更小、更快,但没有任何线程安全机制。它有“非空”和“固定”(不可重定向)版本,可以安全地假定它们始终指向一个有效分配的对象。所以基本上,如果您的目标对象在异步线程之间共享,则使用std::shared_ptr,否则mse::TRefCountingPointer 更佳。

mse::TScopeOwnerPointerboost::scoped_ptr 类似,但在类似于std::shared_ptrstd::weak_ptr 的“强弱”指针关系中与mse::TScopeFixedPointer 结合使用。

mse::TScopeFixedPointer 指向在堆栈上分配的对象,或者其“拥有”指针在堆栈上分配。它(故意)限制了它的功能,以增强编译时安全性而没有运行时成本。 “范围”指针的要点本质上是识别一组足够简单和确定性的环境,不需要(运行时)安全机制。

mse::TRegisteredPointer 的行为类似于原始指针,不同之处在于它的值在目标对象被销毁时自动设置为 null_ptr。在大多数情况下,它可以用作原始指针的一般替代品。像原始指针一样,它没有任何内在的线程安全性。但作为交换,它可以毫无问题地定位在堆栈上分配的对象(并获得相应的性能优势)。启用运行时检查时,此指针可以安全地访问无效内存。因为mse::TRegisteredPointer 在指向有效对象时具有与原始指针相同的行为,所以可以使用编译时指令“禁用”(自动替换为相应的原始指针),从而允许使用它来帮助捕获调试/测试/测试模式,而在发布模式下不会产生任何开销。

Here 是一篇描述为什么以及如何使用它们的文章。 (注意,不要脸的插件。)

【讨论】:

以上是关于有哪些 C++ 智能指针实现可用?的主要内容,如果未能解决你的问题,请参考以下文章

C++单线程智能指针实现

C++智能指针简单剖析

C++智能指针原理与实现

C++ 几种智能指针的简单实现

02 | 自己动手,实现C++的智能指针

C++智能指针的作用,模拟实现auto_ptr,scoped_ptr,shared_ptr