【问题标题】:C++ memory management paradigmsC++ 内存管理范例
【发布时间】:2014-10-01 03:50:08
【问题描述】:

我正在从 C 迁移到 C++11,并试图找出 C++11 程序(或任何具有内置异常的现代语言)的内存管理范例。具体来说,我在游戏开发方面遇到了难题,其中内存耗尽是一个真正的问题。

在 C 中,我习惯于检查 malloc 的返回值;并且通常使用自定义分配器。

对于 C++,我很困惑;虽然我喜欢 STL 容器是如何构建允许自定义分配器的。由于 STL 容器都管理自己的内存,因此简单地向向量添加元素可能会抛出 std::bad_alloc。我该如何防范这样的事情?我听说将所有 throwing 调用包装在 try/catch 块中可能会非常昂贵。

但是,允许异常在调用堆栈中向上传播会导致一堆无法完全执行的函数,并会导致一些非常棘手的代码。即如果A->B->C->D 是一个调用堆栈,D 抛出,A 捕获,那么BCD 可能会因为无法正常完成执行而产生一些奇怪的问题。

此外,nothrow 参数似乎允许非常类似 C 的代码;虽然我现在看不到普通 malloc 的好处。

编写异常安全的 C++ 代码以防止内存不足问题有哪些最佳实践?

编辑:A relevant answer on progammers.stackexchange arguing for exception-less C++ design in consoles.不确定这些论点是否仍然适用于8th generation consoles

【问题讨论】:

  • 您知道某些平台会很高兴地从malloc() 返回一个非空指针,并且一旦您实际使用该内存仍然会失败(因为虚拟内存处理程序仅然后意识到它的交换空间不足)?
  • @DevSolar 是的,有人告诉我这是一种可能性。幸运的是我从来没有遇到过这个问题——不知道在这种情况下我会做什么,除了保持足够大的缓冲区在这种情况下解除分配以便能够有一些回旋空间来适当退出

标签: c++ c++11 memory-management stl


【解决方案1】:

我的回答将更多地针对游戏开发,因为这是我的背景,也是您感兴趣的一部分。不同类型的应用程序会有不同的要求。

游戏通常会预先分配所有动态内存并保持在该预算范围内。尤其是游戏机有硬内存限制,大多数游戏都想使用所有这些。

预先分配所有内容有几个原因。

一,性能。内存分配很慢。你想不惜一切代价避免它。如果您预先分配所有内容,那么您可以编写自定义的高性能内存分配器,如池分配器、堆栈分配器等,它们只是从您预先分配的缓冲区中获取内存。为手头的任务选择最佳分配器很重要。

第二,如果没有足够的内存供您的游戏使用,您会很快知道。在开发过程中,如果内存不足并且需要调整使用情况,您会崩溃,但最终版本不应该崩溃,因为您已经预先分配并停留在内存预算内。

对于异常,许多(但不是全部)游戏禁用异常,同样是出于性能原因。事实上,一些控制台编译器甚至不支持异常。然后,您要么需要毫无例外地使用 STL 库,要么实现自己的容器。许多游戏团队出于性能原因选择实现自己的,以及将它们与自定义内存分配器更好地集成。

也就是说,动态内存分配、STL 和异常对于较小的个人项目/游戏可能非常合适,但请记住对于大型、高性能、实时游戏来说是必要的。

为了异常安全,我肯定会使用RAII。这就是它的目的。另外我建议使用像std::unique_ptrstd::shared_ptr 这样的智能指针来进行内存管理。再加上 RAII,如果你的构造函数抛出,内存将被释放。

【讨论】:

  • 我还推荐阅读有关 Electonic Arts STL 的文章:open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2271.html
  • 出于性能原因禁用异常是愚蠢的。如果处理得当,异常可以胜过处理错误的错误代码(c 方式)。
  • @megadan 是的,我刚刚读到这个。在 exceptional 情况下,异常似乎非常慢 - 在正常情况下仍然会增加一些开销,即使是“零成本”异常。
  • @Rob K 我的 cmets 专门用于游戏开发。性能关键代码不会到处都是错误代码和返回检查,就像它不能被抛出的异常填充一样。发布的游戏不应该失败。如果他们这样做,通常意味着他们必须立即退出。大多数故障必须在开发过程中处理。
  • kevinarpe 发布的关于 EA 的文章有一个很好的部分关于禁用游戏中的异常处理。这是来自EA自己的。同样,这是特定于游戏的,不一定适用于其他领域的最佳实践。
【解决方案2】:

在退出作用域时使用析构函数自动清理。这称为 RAIIResource Acquisition Is Initializion,尽管首字母缩略词并不是最好的。所有标准容器等都会自动清理。

在基于垃圾回收的 C# 和 Java 等语言中,您可以在代码中添加try 块和“使用”语句。 Java 刚刚得到它(try 语法 IIRC 的一部分);它从一开始就在 C# 中(关键字using);在 Python 中它被称为with;而 C++ 没有它,也不需要它。我曾经为 C++ 创建了一个 WITH 宏,这是一个聪明的小技巧,我认为我会一直使用它,但我没有使用过一次,只是在创建它之后立即尝试它:在 C++ RAII 中做到了全部。

总结:使用 RAII,即使用析构函数,然后让这些异常传播。


关于内存耗尽,通常只是认为“我们已经完成了”,除了尽可能有序地终止之外,什么都做不了。

但是也许可以留出一点缓冲区,当内存耗尽时可以释放它,以便有一些工作内存用于清理工作。


C++ 不区分 hard 异常(致命的,例如内存耗尽)和 soft 异常(非致命的一般故障)。

【讨论】:

  • +1。这解释了如何清理问题 A->B->C-D 中的调用堆栈:即使 D 抛出,D、C 和 B 中的析构函数也会运行。这是 Java 的 finally 的清理等效项,但在 C++ 中,清理是在每种类型中指定的,而不是每个使用该类型的函数。这是净节省。
  • 同意,我喜欢 RAII 的想法。感谢您的帖子干杯
  • 另一个使 RAII 比使用 finally 块更好的东西,没有 RAII 在每个使用对象实例的地方,你必须有一个 finally 块,在此之后执行完全相同的事情来清理对象,这违反了 Don't Repeat Yourself (DRY) 原则。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-03
  • 2012-01-24
  • 2010-09-06
  • 2018-12-17
相关资源
最近更新 更多