C ++:智能指针,原始指针,没有指针?[关闭]


48

在用C ++开发游戏的范围内,关于指针的使用,您偏爱的首选模式是什么(无论是无,原始,范围,共享还是在聪明与愚蠢之间使用指针)?

您可能会考虑

  • 对象所有权
  • 使用方便
  • 复制政策
  • 高架
  • 循环引用
  • 目标平台
  • 与容器一起使用

Answers:


32

在尝试了各种方法之后,今天我发现自己与《 Google C ++样式指南》保持一致

如果您确实需要指针语义,那么scoped_ptr很棒。您仅应在非常特定的条件下使用std :: tr1 :: shared_ptr,例如当对象需要由STL容器保存时。您永远不要使用auto_ptr。[...]

一般来说,我们更喜欢设计具有明确对象所有权的代码。通过将对象直接用作字段或局部变量,而完全不使用指针,可以获得最清晰的对象所有权。[..]

尽管不推荐使用它们,但引用计数的指针有时是解决问题的最简单,最优雅的方法。


14
今天,您可能想使用std :: unique_ptr而不是scoped_ptr。
克拉姆2011年

24

我也遵循“强所有权”的思路。我想清楚地描述“适当的时候,这个阶级拥有这个成员”。

我很少使用shared_ptr。如果可以的话,我会weak_ptr尽可能地自由使用它,这样我就可以像对待对象的句柄一样对待它,而不用增加引用计数。

scoped_ptr到处都用。它显示出明显的所有权。我不仅仅使像这样的对象成为对象的唯一原因是,如果它们在scoped_ptr中,则可以向前声明它们。

如果我需要对象列表,请使用ptr_vector。与使用相比,它更有效且副作用更少vector<shared_ptr>。我认为您可能无法在ptr_vector中声明该类型(已经有一段时间了),但是在我看来,它的语义使其值得。基本上,如果您从列表中删除一个对象,它将自动被删除。这也显示出明显的所有权。

如果需要引用某些内容,请尝试使其成为引用,而不是裸露的指针。有时这是不切实际的(即在构造对象之后的任何时候都需要引用)。无论哪种方式,引用都明显表明您不拥有该对象,并且如果您在其他所有地方都遵循共享指针的语义,则裸指针通常不会引起任何其他混淆(尤其是如果您遵循“禁止手动删除”的规则) 。

通过这种方法,我开发的一款iPhone游戏只能进行一次delete调用,而这正是我编写的Obj-C到C ++的桥梁。

总的来说,我认为内存管理太重要了而不能留给人类。如果可以自动删除,则应该删除。如果shared_ptr的开销在运行时过于昂贵(假设您关闭了线程支持等),则可能应该使用其他方法(例如存储桶模式)来降低动态分配。


1
优秀的总结。您实际上是指shared_ptr而不是您提到的smart_ptr吗?
2010年

是的,我的意思是shared_ptr。我会解决的。
四分

10

使用正确的工具完成工作。

如果程序会引发异常,请确保您的代码可识别异常。使用智能指针,RAII和避免两阶段构建是一个很好的起点。

如果您有没有明确所有权语义的循环引用,则可以考虑使用垃圾回收库或重构设计。

好的库可以使您对概念进行编码,而不是对类型进行编码,因此在大多数情况下,除了资源管理问题外,使用哪种类型的指针都没有关系。

如果您在多线程环境中工作,请确保您了解对象是否可能在线程间共享。考虑使用boost :: shared_ptr或std :: tr1 :: shared_ptr的主要原因之一是因为它使用了线程安全的引用计数。

如果您担心引用计数的单独分配,可以采用多种方法来解决。使用boost :: shared_ptr库,您可以池分配参考计数器,或者使用boost :: make_shared(我的偏好),该方法在单个分配中分配对象和参考计数,从而减轻了人们对大多数高速缓存未命中的担忧。通过将对对象的引用保留在最顶层,并将直接引用传递给该对象,可以避免在性能关键代码中更新引用计数而导致性能下降。

如果您需要共享所有权,但又不想支付引用计数或垃圾回收的费用,请考虑使用不可变对象或写成语副本。

请记住,您获得的最大性能胜利将远不止于体系结构级别,其次是算法级别,尽管这些低级别的关注非常重要,但只有在解决了主要问题之后才应予以解决。如果您要在高速缓存未命中级别上处理性能问题,那么您就必须注意很多问题,例如错误共享,这与每个指针无关。

如果您仅使用智能指针来共享纹理或模型等资源,请考虑使用更专业的库,例如Boost.Flyweight。

一旦采用了新标准,移动语义,右值引用和完美的转发将使使用昂贵的对象和容器变得更加容易和高效。在此之前,请勿将具有破坏性复制语义的指针(例如auto_ptr或unique_ptr)存储在Container(标准概念)中。考虑使用Boost.Pointer容器库或将共享所有权智能指针存储在容器中。在对性能有要求的代码中,您可以考虑避免使用Boost.Intrusive等侵入性容器来同时避免这两种情况。

目标平台不应真正影响您的决定。嵌入式设备,智能电话,哑电话,PC和控制台都可以很好地运行代码。项目要求(例如严格的内存预算或加载之前/之后没有动态分配)是更有效的考虑因素,应该会影响您的选择。


3
控制台上的异常处理可能有点狡猾-特别是XDK有点异常-恶意。
Crashworks 2010年

1
目标平台确实应该影响您的设计。转换数据的硬件有时会对源代码产生很大影响。PS3体系结构是一个具体的示例,您确实需要使用硬件来设计资源,内存管理以及渲染器。
西蒙2010年

我仅略有不同意,特别是关于GC。在大多数情况下,循环引用对于引用计数方案而言不是问题。通常,出现这些周期性所有权问题是因为人们没有正确考虑对象的所有权。仅仅因为一个对象需要指向某个东西,并不意味着它应该拥有该指针。通常引用的示例是树中的向后指针,但是树中指针的父级可以安全地成为原始指针,而不会牺牲安全性。
Tim Seguine 2013年

4

如果您使用的是C ++ 0x,请使用std::unique_ptr<T>

它没有性能开销,与std::shared_ptr<T>引用计数开销不同。unique_ptr 拥有其指针,您可以使用C ++ 0x的move语义转移所有权。您无法复制它们-只能移动它们。

它也可以在容器中使用,例如std::vector<std::unique_ptr<T>>,它是二进制兼容的,性能与相同std::vector<T*>,但是如果您擦除元素或清除向量,则不会泄漏内存。与STL算法相比,这也具有更好的兼容性ptr_vector

从很多方面来说,IMO都是理想的容器:随机访问,异常安全,防止内存泄漏,向量重新分配的开销低(只需在幕后的指针周围随机播放)。对于许多用途非常有用。


3

最好的做法是记录哪些类拥有哪些指针。最好只使用普通对象,并且尽可能不使用指针。

但是,当您需要跟踪资源时,传递指针是唯一的选择。有一些情况:

  • 您从其他地方获得了指针,但是却无法对其进行管理:只需使用普通的指针并将其记录下来,以便在尝试删除它之后不再使用编码器。
  • 您从其他地方获取了指针,并对其进行了跟踪:使用scoped_ptr。
  • 您可以从其他地方获取指针,并对其进行跟踪,但是它需要一种特殊的方法来删除它:将shared_ptr与自定义delete方法一起使用。
  • 您需要将指针放在STL容器中:它将在周围复制,因此需要boost :: shared_ptr。
  • 许多类共享该指针,尚不清楚是谁将其删除:shared_ptr(以上情况实际上是这一点的一种特殊情况)。
  • 您可以自己创建指针,只需要它即可:如果确实不能使用普通对象:scoped_ptr。
  • 您将创建指针,并将其与其他类共享:shared_ptr。
  • 您创建并传递指针:使用普通指针并记录您的接口,以便新所有者知道他应该亲自管理资源!

我认为这几乎涵盖了我现在如何管理资源。像shared_ptr这样的指针的内存开销通常是普通指针的两倍。我认为开销不会太大,但是如果资源不足,则应考虑设计游戏以减少智能指针的数量。在其他情况下,我只是按照上面的项目符号设计好的原则,分析器会告诉我在哪里我需要更高的速度。


1

特别是在谈到boost的指针时,我认为应该避免使用它们,只要它们的实现不完全是您所需要的即可。它们的代价确实比任何人最初预期的都要大。它们提供了一个界面,使您可以跳过内存和资源管理中的重要部分。

对于任何软件开发,我认为考虑数据非常重要。如何在内存中表示数据非常重要。原因是CPU速度的增长速度远远超过内存访问时间。这通常使内存缓存成为大多数现代计算机游戏的主要瓶颈。通过根据访问顺序将数据线性排列在内存中,对缓存更加友好。这种解决方案通常会导致设计更简洁,代码更简单,定义明确的代码更易于调试。智能指针容易导致频繁的动态内存分配,这导致它们分散在整个内存中。

这不是过早的优化,这是一个健康的决定,可以并且应该尽早采取。这是您的软件将在其上运行的硬件的架构理解的问题,这一点很重要。

编辑:关于共享指针的性能,需要考虑几件事:

  • 参考计数器是堆分配的。
  • 如果启用了线程安全性,则引用计数是通过互锁操作完成的。
  • 按值传递指针会修改引用计数,这意味着最有可能使用内存中的随机访问(锁定+可能的高速缓存未命中)进行互锁的操作。

2
您不惜一切代价避免了我。然后,您将继续描述一种对于现实世界游戏很少关注的优化类型。大多数游戏开发的特征是开发问题(延迟,错误,可玩性等),而不是缺乏CPU缓存性能。因此,我强烈不同意这种建议不是过早的优化的想法。
kevin42

2
我必须同意数据布局的早期设计。从现代的控制台/移动设备中获得任何性能都是很重要的,这一点永远都不应忽视。
Olly 2010年

1
这是我一直在工作的AAA工作室中遇到的一个问题。您还可以收听Insomniac Games的首席架构师Mike Acton。我并不是说boost是一个不好的库,它不仅非常适合高性能游戏。
西蒙2010年

1
@ kevin42:缓存一致性可能是当今游戏开发中低级优化的主要来源。@Simon:大多数shared_ptr实现都避免在任何支持比较和交换的平台上锁定,包括Linux和Windows PC,我相信包括Xbox。

1
@Joe Wreschnig:的确如此,尽管会导致共享指针的任何初始化(复制,从弱指针创建等),但仍然很有可能发生高速缓存丢失。现代PC上的L2缓存丢失大约为200个周期,而在PPC(xbox360 / ps3)上则更高。在激烈的游戏中,您可能最多拥有1000个游戏对象,鉴于每个游戏对象可以拥有很多资源,我们正在研究缩放比例是主要关注的问题。在开发周期结束时(当您击中大量游戏对象时),这可能会引起问题。
西蒙2010年

0

我倾向于在各处使用智能指针。我不确定这是否是个好主意,但是我很懒,而且我看不到任何实际的缺点[除非我想做一些C风格的指针算术]。我使用boost :: shared_ptr是因为我知道我可以复制它-如果两个实体共享一个图像,那么如果一个实体死亡,另一个实体也不应丢失该图像。

不利的一面是,如果一个对象删除了它指向并拥有的某个对象,但是其他对象也指向了它,那么它就不会被删除。


1
我也几乎在所有地方都使用了share_ptr-但是今天,我尝试考虑是否确实需要对某些数据共享所有权。如果不是,则使该数据成为父数据结构的非指针成员可能是合理的。我发现明确的所有权简化了设计。
jmp97 2010年

0

好的智能指针提供的内存管理和文档的好处意味着我经常使用它们。但是,当探查器启动并告诉我特定的用法使我不知所措时,我将恢复为新石器时代的指针管理。


0

我很老,很老,还有一个自行车计数器。在我自己的工作中,我使用原始指针,并且在运行时不使用动态分配(池本身除外)。一切都汇集在一起​​,所有权非常严格且永远不可转让,如果确实需要,我编写了一个自定义的小块分配器。我确保游戏期间每个池都有一个状态来清除自身。当事情变得繁琐时,我确实将对象包裹在句柄中,以便可以重新放置它们,但我宁愿不这样做。容器是定制的,非常裸露。我也不重用代码。
尽管我永远不会争论所有智能指针,容器,迭代器以及诸如此类的优点,但我以能够极其快速地进行编码(并且相当可靠)而著称,尽管出于某些显而易见的原因,不建议其他人跳入我的代码,像是令人心碎的攻击和永恒的噩梦)。

当然,在工作中,一切都是不同的,除非我要制作原型,我很高兴能做很多工作。


0

尽管几乎没有人承认这是一个奇怪的答案,而且可能还远远没有适合每个人的答案。

但是我发现,就我个人而言,将特定类型的所有实例存储在中央的随机访问序列(线程安全)中,而不是与32位索引(相对地址)一起工作,会更加有用。 ,而不是绝对指针。

作为一个开始:

  1. 它在64位平台上将模拟指针的内存需求减少了一半。到目前为止,我从未需要超过42.9亿个特定数据类型的实例。
  2. 它可以确保所有特定类型的实例T永远不会在内存中分散。如果节点使用索引而不是指针链接在一起,则即使是遍历树之类的链接结构,也倾向于减少各种访问模式的高速缓存未命中率。
  3. 使用便宜的并行数组(或稀疏数组)而不是树或哈希表,可以轻松关联并行数据。
  4. 设置交集可以在线性时间内找到,也可以使用并行位集更好地找到。
  5. 我们可以对索引进行基数排序,并获得非常易于缓存的顺序访问模式。
  6. 我们可以跟踪多少实例分配了特定数据类型。
  7. 如果您关心此类事情,则将必须处理诸如异常安全之类的事情的数量减至最少。

也就是说,便利性和类型安全性都是缺点。我们不能访问的实例T,而无需访问这两个容器索引。一个普通的旧文件int32_t无法告诉我们它所指向的数据类型,因此没有类型安全性。我们可能会意外地尝试Bar使用索引访问Foo。为了减轻第二个问题,我经常做这种事情:

struct FooIndex
{
    int32_t index;
};

这似乎很愚蠢,但是它使我恢复了类型安全性,这样人们就不会BarFoo没有编译器错误的情况下意外地尝试通过索引访问a 。为了方便起见,我只接受不便之处。

可能给人们带来很大不便的另一件事是,我不能使用基于OOP风格的基于继承的多态性,因为这将需要一个基指针,该基指针可以指向具有不同大小和对齐要求的各种不同的子类型。但是这些天我很少使用继承-更喜欢ECS方法。

至于shared_ptr,我尽量不要使用太多。在大多数情况下,我认为共享所有权是没有道理的,而无意间这样做会导致逻辑泄漏。通常至少在高层次上,一件事倾向于属于一件事。我经常发现很想使用它的shared_ptr地方是延长对象的寿命,而这些地方实际上并没有太多地处理所有权,例如只是在线程中使用局部函数来确保对象在线程完成之前不会被销毁使用它。

为了解决该问题,shared_ptr我通常倾向于使用从线程池运行的短期任务,而不是使用GC或类似的方法,因此,如果该线程请求销毁对象,则将实际销毁推迟到安全的时间进行系统可以确保没有线程需要访问所述对象类型的时间。

我有时仍然确实会使用引用计数,但是将其视为万不得已的策略。在某些情况下,真正意义上共享所有权是有意义的,例如持久数据结构的实现,而且我确实发现立即实现所有权是完全有意义的shared_ptr

因此,无论如何,我主要使用索引,并且很少使用原始指针和智能指针。当您知道对象是连续存储的而不是分散在存储空间中时,我喜欢索引及其打开的门的种类。

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.