为什么 C + + 库和框架从不使用智能指针?

我在几篇文章中读到,几乎不应该使用原始指针。相反,它们应该总是包装在智能指针中,不管它是作用域指针还是共享指针。

然而,我注意到像 Qt、 wxWidgets 这样的框架和像 Boost 这样的库永远不会返回,也不会期待智能指针,就好像他们根本不使用它们一样。相反,它们返回或期待原始指针。有什么原因吗?在编写公共 API 时,我应该远离智能指针吗? 为什么?

只是想知道为什么推荐智能指针,而许多主要项目似乎都避免使用它们。

27519 次浏览

原因有很多,下面列举几个:

  1. 智能指针最近才成为标准的一部分 是其他图书馆的一部分
  2. 它们的主要用途是避免内存泄漏; 许多库 没有自己的内存管理; 通常他们提供 工具及应用程式介面
  3. 它们实现为包装器,因为它们实际上是对象而不是指针。与原始指针相比,它具有额外的时间/空间成本; 库的用户可能不希望有这样的开销

编辑 : 使用智能指针完全是开发人员的选择,它取决于各种因素。

  1. 在性能关键系统中,您可能不希望使用智能 生成开销的指针

  2. 需要向下兼容的项目,你可能不想要 使用具有 C + + 11特定功能的智能指针

编辑2 由于以下通过,在24小时内有一连串的否决票。我不明白为什么答案是否定的,即使下面只是一个附加的建议,而不是一个答案。
但是,C + + 总是使您能够打开. :)选项。

template<typename T>
struct Pointer {
#ifdef <Cpp11>
typedef std::unique_ptr<T> type;
#else
typedef T* type;
#endif
};

并且在您的代码中将其用作:

Pointer<int>::type p;

对于那些说智能指针和原始指针不同的人,我同意这一点。上面的代码只是一个 主意,其中一个人可以写一个代码是可互换的只是一个 #define,这不是 强迫;

例如,必须显式删除 T*,但智能指针不需要。我们可以有一个模板 Destroy()来处理这个问题。

template<typename T>
void Destroy (T* p)
{
delete p;
}
template<typename T>
void Destroy (std::unique_ptr<T> p)
{
// do nothing
}

并将其用作:

Destroy(p);

同样,对于一个原始指针,我们可以直接复制它,对于智能指针,我们可以使用特殊的操作。

Pointer<X>::type p = new X;
Pointer<X>::type p2(Assign(p));

其中 Assign()为:

template<typename T>
T* Assign (T *p)
{
return p;
}
template<typename T>
... Assign (SmartPointer<T> &p)
{
// use move sematics or whateve appropriate
}

问得好。我不知道你指的是哪些具体的文章,但我时常读到类似的东西。我怀疑这些文章的作者对 C + + 风格的编程有偏见。如果编写者只在必要的时候才用 C + + 编程,那么他就会尽可能快地返回到 Java 之类的程序中,那么他就不会真正地采用 C + + 的思维模式。

有人怀疑,一些或大多数相同的作者更喜欢垃圾收集内存管理器。我不知道,但我的想法和他们不一样。

智能指针很棒,但它们必须保持引用计数。参考计数的保留承担了运行时的成本——通常是适度的成本,但仍然是成本。通过使用空指针来节省这些开销并没有错,特别是如果指针是由析构函数管理的话。

C + + 的一个优点是它支持嵌入式系统编程。空指针的使用是其中的一部分。

更新: 一个评论者已经正确地观察到 C + + 的新 unique_ptr(从 TR1开始就可用)不计算引用。评论者对“智能指针”的定义也与我想象的不同。他的定义可能是对的。

进一步更新: 下面的评论很有启发性,推荐阅读。

Qt 毫无意义地重新发明了标准库的许多部分,试图成为 Java。我相信它现在确实有自己的智能指南,但总的来说,它几乎不是设计的顶峰。WxWidgets,据我所知,早在编写可用的智能指针之前就被设计出来了。

至于 Boost,我完全希望他们在适当的地方使用智能指针。

另外,不要忘记智能指针的存在是为了强制所有权。如果 API 没有所有权语义,那么为什么要使用智能指针呢?

智能指针(pre C + + 11)有两个问题:

  • 非标准,因此每个库都倾向于重新发明自己的(NIH 综合症和依赖性问题)
  • 潜在成本

违约智能指针是 unique_ptr,因为它是免费的。不幸的是,它需要 C + + 11 move 语义,这是最近才出现的。所有其他智能指针都有代价(shared_ptrintrusive_ptr)或者语义不理想(auto_ptr)。

随着 C + + 11的到来,带来了一个 std::unique_ptr,人们可能会认为它终于结束了... ... 我不是那么乐观。

只有少数几个主要的编译器实现了大部分 C + + 11,而且只是在它们最近的版本中。我们可以预期主要的库,如 QT 和 Boost,愿意在一段时间内保持与 C + + 03的兼容性,这在一定程度上阻碍了新的闪亮智能指针的广泛采用。

您不应该远离智能指针,它们有自己的用途,特别是在需要传递对象的应用程序中。

库往往要么只返回一个值,要么填充一个对象。它们通常没有需要在很多地方使用的对象,因此不需要使用智能指针(至少不在接口中,它们可以在内部使用)。

我可以举一个我们一直在研究的库为例,经过几个月的开发,我意识到我们只在几个类中使用指针和智能指针(占所有类的3-5%)。

在大多数情况下,传递 引用变量就足够了,只要我们有一个可以为空的对象,我们就使用智能指针; 当我们使用的库强迫我们使用原始指针时,我们就使用原始指针。

编辑 (我不能评论,因为我的名声) : 通过引用传递变量是非常灵活的: 如果你希望对象是只读的,你可以使用常量引用(你仍然可以做一些讨厌的强制转换来编写对象) ,但是你可以得到最大限度的保护(智能指针也是一样)。 但我同意只返回对象要好得多。

除了许多库是在标准智能指针出现之前编写的这一事实之外,最大的原因可能是缺乏标准的 c + + 应用二进制接口(ABI)。

如果您正在编写一个只有标题的库,那么您可以将智能指针和标准容器传递给您想要的内容。它们的源代码在编译时可用于您的库,因此您仅仅依赖于它们的接口的稳定性,而不是它们的实现的稳定性。

但是由于缺乏标准的 ABI,您通常可以安全地跨模块边界传递这些对象。GCC shared_ptr可能与 MSVC shared_ptr不同,MSVC shared_ptr也可能与英特尔 shared_ptr不同。即使使用 一样编译器,也不能保证这些类在不同版本之间是二进制兼容的。

底线是,如果您想要发布库的 预制的版本,您需要一个标准的 ABI 来依赖。C 没有,但是编译器供应商非常重视特定平台上 C 库之间的互操作性ーー事实上有标准。

对于 C + + 来说,情况就没有那么好了。单个编译器可以处理它们自己的二进制文件之间的互操作,因此您可以选择为每个受支持的编译器(通常是 GCC 和 MSVC)分发一个版本。但有鉴于此,大多数库只导出 C 接口ーー这意味着原始指针。

但是,非库代码通常更喜欢智能指针而不是原始代码。

还有其他类型的智能指针。你可能需要一个专门的智能指针来进行网络复制(一个检测是否被访问并向服务器发送任何修改的指针) ,保存更改的历史记录,标记被访问的事实,这样当你将数据保存到磁盘时就可以进行调查,等等。不确定在指针中这样做是否是最好的解决方案,但是在库中使用内置的智能指针类型可能会导致人们被锁定在这些类型中并失去灵活性。

除了智能指针,人们还可以有各种不同的内存管理需求和解决方案。我可能想自己管理内存,我可以为内存池中的东西分配空间,这样就可以提前而不是在运行时分配(对于游戏来说很有用)。我可能使用了 C + + 的一个垃圾回收实现(C + + 11使这成为可能,尽管目前还没有这样的实现)。或者也许我只是没有做足够高级的事情来担心与他们打交道,我可以知道我不会忘记未初始化的对象等等。也许我只是对自己在没有指针的情况下管理内存的能力有信心。

与 C 的集成也是另一个问题。

另一个问题是智能指针是 STL 的一部分。 C + + 被设计成可以在没有 STL 的情况下使用。

它还取决于您在哪个领域工作。我以写游戏引擎为生,我们避免像瘟疫一样的增益,在游戏中增益的开销是不可接受的。在我们的核心引擎中,我们最终编写了自己的 stl 版本(很像 ea stl)。

如果我要编写一个表单应用程序,我可能会考虑使用智能指针; 但是一旦内存管理成为第二天性,没有对内存的粒度控制就会变得非常烦人。