Answers:
在尝试了各种方法之后,今天我发现自己与《 Google C ++样式指南》保持一致:
如果您确实需要指针语义,那么scoped_ptr很棒。您仅应在非常特定的条件下使用std :: tr1 :: shared_ptr,例如当对象需要由STL容器保存时。您永远不要使用auto_ptr。[...]
一般来说,我们更喜欢设计具有明确对象所有权的代码。通过将对象直接用作字段或局部变量,而完全不使用指针,可以获得最清晰的对象所有权。[..]
尽管不推荐使用它们,但引用计数的指针有时是解决问题的最简单,最优雅的方法。
我也遵循“强所有权”的思路。我想清楚地描述“适当的时候,这个阶级拥有这个成员”。
我很少使用shared_ptr
。如果可以的话,我会weak_ptr
尽可能地自由使用它,这样我就可以像对待对象的句柄一样对待它,而不用增加引用计数。
我scoped_ptr
到处都用。它显示出明显的所有权。我不仅仅使像这样的对象成为对象的唯一原因是,如果它们在scoped_ptr中,则可以向前声明它们。
如果我需要对象列表,请使用ptr_vector
。与使用相比,它更有效且副作用更少vector<shared_ptr>
。我认为您可能无法在ptr_vector中声明该类型(已经有一段时间了),但是在我看来,它的语义使其值得。基本上,如果您从列表中删除一个对象,它将自动被删除。这也显示出明显的所有权。
如果需要引用某些内容,请尝试使其成为引用,而不是裸露的指针。有时这是不切实际的(即在构造对象之后的任何时候都需要引用)。无论哪种方式,引用都明显表明您不拥有该对象,并且如果您在其他所有地方都遵循共享指针的语义,则裸指针通常不会引起任何其他混淆(尤其是如果您遵循“禁止手动删除”的规则) 。
通过这种方法,我开发的一款iPhone游戏只能进行一次delete
调用,而这正是我编写的Obj-C到C ++的桥梁。
总的来说,我认为内存管理太重要了而不能留给人类。如果可以自动删除,则应该删除。如果shared_ptr的开销在运行时过于昂贵(假设您关闭了线程支持等),则可能应该使用其他方法(例如存储桶模式)来降低动态分配。
使用正确的工具完成工作。
如果程序会引发异常,请确保您的代码可识别异常。使用智能指针,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和控制台都可以很好地运行代码。项目要求(例如严格的内存预算或加载之前/之后没有动态分配)是更有效的考虑因素,应该会影响您的选择。
如果您使用的是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都是理想的容器:随机访问,异常安全,防止内存泄漏,向量重新分配的开销低(只需在幕后的指针周围随机播放)。对于许多用途非常有用。
最好的做法是记录哪些类拥有哪些指针。最好只使用普通对象,并且尽可能不使用指针。
但是,当您需要跟踪资源时,传递指针是唯一的选择。有一些情况:
我认为这几乎涵盖了我现在如何管理资源。像shared_ptr这样的指针的内存开销通常是普通指针的两倍。我认为开销不会太大,但是如果资源不足,则应考虑设计游戏以减少智能指针的数量。在其他情况下,我只是按照上面的项目符号设计好的原则,分析器会告诉我在哪里我需要更高的速度。
特别是在谈到boost的指针时,我认为应该避免使用它们,只要它们的实现不完全是您所需要的即可。它们的代价确实比任何人最初预期的都要大。它们提供了一个界面,使您可以跳过内存和资源管理中的重要部分。
对于任何软件开发,我认为考虑数据非常重要。如何在内存中表示数据非常重要。原因是CPU速度的增长速度远远超过内存访问时间。这通常使内存缓存成为大多数现代计算机游戏的主要瓶颈。通过根据访问顺序将数据线性排列在内存中,对缓存更加友好。这种解决方案通常会导致设计更简洁,代码更简单,定义明确的代码更易于调试。智能指针容易导致频繁的动态内存分配,这导致它们分散在整个内存中。
这不是过早的优化,这是一个健康的决定,可以并且应该尽早采取。这是您的软件将在其上运行的硬件的架构理解的问题,这一点很重要。
编辑:关于共享指针的性能,需要考虑几件事:
我倾向于在各处使用智能指针。我不确定这是否是个好主意,但是我很懒,而且我看不到任何实际的缺点[除非我想做一些C风格的指针算术]。我使用boost :: shared_ptr是因为我知道我可以复制它-如果两个实体共享一个图像,那么如果一个实体死亡,另一个实体也不应丢失该图像。
不利的一面是,如果一个对象删除了它指向并拥有的某个对象,但是其他对象也指向了它,那么它就不会被删除。
我很老,很老,还有一个自行车计数器。在我自己的工作中,我使用原始指针,并且在运行时不使用动态分配(池本身除外)。一切都汇集在一起,所有权非常严格且永远不可转让,如果确实需要,我编写了一个自定义的小块分配器。我确保游戏期间每个池都有一个状态来清除自身。当事情变得繁琐时,我确实将对象包裹在句柄中,以便可以重新放置它们,但我宁愿不这样做。容器是定制的,非常裸露。我也不重用代码。
尽管我永远不会争论所有智能指针,容器,迭代器以及诸如此类的优点,但我以能够极其快速地进行编码(并且相当可靠)而著称,尽管出于某些显而易见的原因,不建议其他人跳入我的代码,像是令人心碎的攻击和永恒的噩梦)。
当然,在工作中,一切都是不同的,除非我要制作原型,我很高兴能做很多工作。
尽管几乎没有人承认这是一个奇怪的答案,而且可能还远远没有适合每个人的答案。
但是我发现,就我个人而言,将特定类型的所有实例存储在中央的随机访问序列(线程安全)中,而不是与32位索引(相对地址)一起工作,会更加有用。 ,而不是绝对指针。
作为一个开始:
T
永远不会在内存中分散。如果节点使用索引而不是指针链接在一起,则即使是遍历树之类的链接结构,也倾向于减少各种访问模式的高速缓存未命中率。也就是说,便利性和类型安全性都是缺点。我们不能访问的实例T
,而无需访问这两个容器和索引。一个普通的旧文件int32_t
无法告诉我们它所指向的数据类型,因此没有类型安全性。我们可能会意外地尝试Bar
使用索引访问Foo
。为了减轻第二个问题,我经常做这种事情:
struct FooIndex
{
int32_t index;
};
这似乎很愚蠢,但是它使我恢复了类型安全性,这样人们就不会Bar
在Foo
没有编译器错误的情况下意外地尝试通过索引访问a 。为了方便起见,我只接受不便之处。
可能给人们带来很大不便的另一件事是,我不能使用基于OOP风格的基于继承的多态性,因为这将需要一个基指针,该基指针可以指向具有不同大小和对齐要求的各种不同的子类型。但是这些天我很少使用继承-更喜欢ECS方法。
至于shared_ptr
,我尽量不要使用太多。在大多数情况下,我认为共享所有权是没有道理的,而无意间这样做会导致逻辑泄漏。通常至少在高层次上,一件事倾向于属于一件事。我经常发现很想使用它的shared_ptr
地方是延长对象的寿命,而这些地方实际上并没有太多地处理所有权,例如只是在线程中使用局部函数来确保对象在线程完成之前不会被销毁使用它。
为了解决该问题,shared_ptr
我通常倾向于使用从线程池运行的短期任务,而不是使用GC或类似的方法,因此,如果该线程请求销毁对象,则将实际销毁推迟到安全的时间进行系统可以确保没有线程需要访问所述对象类型的时间。
我有时仍然确实会使用引用计数,但是将其视为万不得已的策略。在某些情况下,真正意义上共享所有权是有意义的,例如持久数据结构的实现,而且我确实发现立即实现所有权是完全有意义的shared_ptr
。
因此,无论如何,我主要使用索引,并且很少使用原始指针和智能指针。当您知道对象是连续存储的而不是分散在存储空间中时,我喜欢索引及其打开的门的种类。