如果make_shared/make_unique可能抛出bad_alloc异常,为什么不常规地为此使用try catch块呢?

7

CppReference 中 make_shared 的页面说(与 make_unique 相同)

可能会抛出 std::bad_alloc 或 T 类型构造函数抛出的任何异常。 如果抛出异常,该函数将不起作用。

这意味着在发生故障时可能会抛出 std::bad_alloc 异常。"函数将不起作用" 隐含地意味着它不能返回 nullptr。如果是这种情况,为什么人们不经常将 make_shared/make_unique 放到 try catch 块中?

那么,使用 make_shared 的正确方式是什么? 在 try catch 块内吗?还是检查是否为 nullptr?


异常处理(与RAII相结合)的一个好处是,您不必在错误发生的地方处理错误,而可以在调用堆栈中的某个方便的位置进行处理。如果错误是致命的,甚至可以不处理。 - Some programmer dude
2个回答

7
我看到了两个主要原因。
  1. 动态内存分配失败通常被认为是无法优雅处理的情况。程序会被终止,就这样结束了。这意味着我们经常不会检查每一个可能的 std::bad_alloc 异常。或者你是否将 std::vector::push_back 包装在 try-catch 块中,因为底层分配器可能会抛出异常?

  2. 并不是每个可能的异常都必须在调用点立即捕获。有建议称 throwcatch 的关系应该大于一。这意味着你应该在更高的层次上捕获异常,将多个错误路径收集到一个处理程序中。构造函数抛出异常的情况也可以这样处理。毕竟,异常是异常情况。如果在堆上构造对象很容易引发异常,以至于您必须检查每次调用,那么您应该考虑使用不同的错误处理方案(例如 std::optionalstd::expected 等)。

无论如何,检查 nullptr 显然不是确保 std::make_unique 成功的正确方法。它从不返回 nullptr——要么成功,要么抛出异常。


那回答了我的问题@lubgr。谢谢。这是否意味着期望bad_alloc异常并不是必要的,因为它很少出现? - Boanerges
2
我认为这取决于你的目标平台。在普通的64位桌面计算机上,只要不处理大量内存数据,通常不会出现分配失败。但在某些嵌入式设备上可能完全不同。 - lubgr
好的!即使在内存很低的嵌入式设备中,使用通用的try catch机制而不是为每个make_shared使用都使用try/catch块也是有意义的。对吧? - Boanerges
1
你不能一概而论,我认为有些平台甚至没有堆。如果在这样的设备上分配是一个问题,你应该通过使用自定义分配器来集中处理分配失败。 - lubgr

3
抛出bad_alloc有两个效果:
  • 它允许在调用者层次结构中捕获和处理错误。
  • 它会产生明确定义的行为,而不管是否发生了这样的处理。
默认的良好定义行为是通过调用std::terminate()以一种加速但有序的方式终止进程。请注意,是否在调用terminate()之前展开堆栈是由实现定义的(但对于给定的实现,仍然是良好定义的)。
这与未处理的失败malloc()非常不同,例如,当返回的空指针被解引用时,(a)会导致未定义的行为,(b)让执行在那一刻之前和之后轻松进行,通常还会累积更多的分配失败。
那么,接下来的问题是,如果需要的话,调用代码应该在哪里和如何捕获和处理异常。
在大多数情况下,答案是不应该。
处理程序会做什么?真的只有两个选择:
  • 以比默认的未处理异常处理更有序的方式终止应用程序。
  • 在其他某个地方释放一些内存并重试分配。
这两种方法都会向系统添加复杂性(后者尤其如此),需要针对特定情况进行证明 - 而且,重要的是,在其他可能的故障模式和缓解措施的背景下进行证明。 (例如,已经包含非软件容错保护措施的关键系统可能更好地快速终止以让这些机制发挥作用,而不是在软件中乱搞。)
在这两种情况下,实际处理更有意义的方法可能更高地位于使分配失败的点之上。
如果这两种方法都没有任何优势,则最佳方法就是让默认的std::terminate()处理启动

“Simply to let the default handling kick in” - 这句话的意思是什么,@Jeremy? - Boanerges

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接