为什么向量访问运算符没有被指定为noexcept?

54

为什么 std::vectoroperator[]frontback 成员函数没有被指定为 noexcept

3个回答

78
标准的策略是只对不能或不得失败的函数进行noexcept标记,而不是那些简单地规定不会抛出异常的函数。换句话说,所有具有有限域的函数(传递错误的参数会导致未定义行为)都不是noexcept,即使它们没有指定抛出异常。
被标记的函数是像swap(不能失败,因为异常安全通常依赖于它)和numeric_limits :: min(不能失败,返回原始类型的常量)这样的东西。
原因是实现者可能希望提供特殊的调试版本库,以在各种未定义行为情况下引发异常,以便测试框架可以轻松检测错误。例如,如果您在vector :: operator []中使用越界索引,或在空向量上调用frontback。一些实现可能想要在那里抛出异常(他们可以:由于它是未定义行为,他们可以做任何事情),但标准规定的noexcept对这些函数的使用将变得不可能。

2
越界是未定义行为,在未定义行为的情况下,任何事情都可能发生。未定义行为不受 noexcept 的限制。尽管当异常发生时,noexcept 一定会终止,但这在测试框架中可能不是期望的结果。 - PlasmaHH
8
从理论上讲,您是正确的。一旦调用了未定义行为,一个被标记为noexcept的函数实际上可能会抛出异常。然而,考虑到代码生成的实际情况以及这种调试模式将未定义行为转换为已定义行为(抛出异常),使得这种方法不可行于编写C++实现。如果一个函数被标记为noexcept,那么调用者可能会缺少撤销表,因此尝试解开异常将导致崩溃或使执行进入无尽的状态,这就有点达不到抛出异常的目的了。 - Sebastian Redl
那么根据这个逻辑,noexcept函数可以给定任何参数吗? - curiousguy
假设参数对于该类型是有效的,那么如果一个函数是noexcept并且接受一个T,那么该函数声明它不能/不应该在任何T值上失败。基于给出的示例,vector::operator[](i) noexcept可能会像vector::at()一样执行边界检查,并在任何i >= size()的情况下返回一个哨兵值,而不是抛出异常。 - Justin Time - Reinstate Monica

19
作为对@SebastianRedl的回答的补充:为什么你需要noexcept? < h2 > noexcept和std::vector < /h2 > 正如您所知,vector有其容量。如果在push_back时已满,它将分配更大的内存,将所有现有元素复制(自C++11以来移动)到新的主干,然后将新元素添加到末尾。

Use copy constructor to expand a vector

但是如果在分配内存或将元素复制到新的容器时抛出异常怎么办?

  • 如果在分配内存期间抛出异常,则向量仍处于原始状态。只需重新抛出异常并让用户处理即可。

  • 如果在复制现有元素期间抛出异常,则通过调用析构函数销毁所有已复制的元素,释放分配的容器,并将异常抛出以由用户代码处理。(1)
    在销毁所有内容后,向量回到原始状态。现在可以安全地抛出异常以让用户处理,而不会泄漏任何资源。

noexcept 和 move

进入C++ 11时代,我们拥有了一个强大的工具——move。它允许我们从未使用的对象中窃取资源。当std::vector需要增加(或减少)容量时,将使用move只要move操作是noexcept的。

假设在移动过程中出现异常,前一个主干与 move 之前不同:资源被窃取,导致向量处于 破碎 状态。用户无法处理异常,因为一切都处于不确定状态。

Use move constructor to expand a vector

这就是为什么std::vector依赖于移动构造函数来保证不抛出异常
这是一个演示,客户端代码如何依赖于noexcept作为接口规范。如果后来不满足noexcept要求,之前依赖它的任何代码都将被破坏。

为什么不直接将所有函数标记为 noexcept

简短回答:编写异常安全代码很困难。

详细回答:使用 noexcept 会对实现接口的开发人员设置严格限制。如果您想从接口中删除 noexcept,则客户端代码可能会出现问题,就像上面给出的 vector 示例一样;但是如果您想将接口标记为 noexcept,则可以随时自由地这样做。

因此,只有在必要时才将接口标记为 noexcept


Going Native 2013中,Scott Meyers谈到了没有noexcept的情况下,程序的健壮性将会失败。
我也写了一篇关于此的博客:https://xinhuang.github.io/posts/2013-12-31-when-to-use-noexcept-and-when-to-not.html

7
简而言之,有些函数被指定为带有或不带有noexcept。这是有意义的,因为它们是不同的。原则是:指定未定义行为的函数(例如由于不合适的参数)不应该带有noexcept本文明确指定了这些成员不带有noexcept。一些vector的成员被用作示例:

具有广泛协议的函数示例为vector<T>::begin()vector<T>::at(size_type)。没有广泛协议的函数示例为vector<T>::front()vector<T>::operator[](size_type)

有关初始动机和详细讨论,请参见本文。这里最明显的现实问题是可测试性。

同一篇论文中“宽泛契约”的定义:函数或操作的宽泛契约不会指定任何未定义行为。这样的契约没有前置条件:具有宽泛契约的函数不会对其参数、任何对象状态或任何外部全局状态施加额外的运行时约束。 - Samuel Li

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