提问者:小点点

什么时候我应该真正使用noexcept?


noexcept关键字可以适当地应用于许多函数签名,但我不确定何时应该考虑在实际中使用它。根据我到目前为止阅读的内容,最后一分钟添加的noexcept似乎解决了移动构造函数抛出时出现的一些重要问题。但是,对于一些实际问题,我仍然无法提供令人满意的答案,这些问题导致我首先阅读更多关于noexcept的内容。

>

  • 有许多函数的例子,我知道它们永远不会抛出,但编译器无法自行确定这些函数。在所有这种情况下,我是否应该在函数声明中追加noexcept

    必须考虑是否需要在每个函数声明后添加noexcept会大大降低程序员的工作效率(坦率地说,这将是一种痛苦)。对于哪些情况,我应该更小心使用noexcept;对于哪些情况,我可以使用隐含的noexcept(false)

    在使用noexcept后,我什么时候可以实际地看到性能改进?特别是,给出一个代码示例,在添加noexcept之后,C++编译器能够生成更好的机器代码。

    就我个人而言,我关心noexcept,因为编译器安全地应用某些类型的优化提供了更多的自由度。现代编译器是否以这种方式利用noexcept?如果不是,我是否可以期望他们中的一些人会在不久的将来这样做?


  • 共3个答案

    匿名用户

    我认为现在给出一个“最佳实践”的答案还为时尚早,因为还没有足够的时间在实践中使用它。如果在抛出说明符后就问这个问题,那么答案将与现在大不相同。

    必须考虑是否需要在每个函数声明后添加noexcept会大大降低程序员的工作效率(坦率地说,这将是一种痛苦)。

    好吧,那就在函数显然永远不会抛出的时候使用它。

    在使用noexcept后,我什么时候可以实际地看到性能改进?[...]就我个人而言,我关心noexcept,因为编译器安全地应用某些类型的优化提供了更多的自由度。

    似乎最大的优化收益来自用户优化,而不是编译器优化,因为可以检查noexcept并在其上重载。大多数编译器遵循不抛出就不罚的异常处理方法,所以我怀疑它在代码的机器代码级别上会有多大的改变(或任何改变),尽管可能会通过删除处理代码来减少二进制大小。

    在四大类(构造函数、赋值函数,而不是析构函数,因为它们已经是noexcept)中使用noexcept检查可能会带来最好的改进,因为noexcept检查在模板代码(如std容器)中是“常见的”。例如,std::vector不会使用类的移动,除非它被标记为noexcept(或者编译器可以用其他方式推导)。

    匿名用户

    这几天我一直在重复:语义第一。

    添加noexceptnoexcept(true)noexcept(false)首先是关于语义的。它只是附带地限制了一些可能的优化。

    作为一个阅读代码的程序员,noexcept的存在与const的存在类似:它帮助我更好地了解可能发生或不发生的事情。因此,花点时间思考是否知道函数是否会抛出是值得的。对于一个提醒,任何类型的动态内存分配都可能抛出。

    好了,现在来看看可能的优化。

    最明显的优化实际上是在库中执行的。C++11提供了许多特性,允许了解函数是否为noexcept,如果可能的话,标准库实现本身将使用这些特性来支持对其操作的用户定义对象进行noexcept操作。例如移动语义。

    编译器可能只会从异常处理数据中剃掉一点脂肪(也许),因为它必须考虑到您可能说谎的事实。如果标记为noexcept的函数抛出,则调用std::terminate

    选择这些语义有两个原因:

    • 即使依赖项尚未使用noexcept(向后兼容性)
    • 也会立即受益于noexcept(向后兼容)
    • 允许在调用理论上可能抛出但对于给定参数不期望抛出的函数时使用noexcept的规范

    匿名用户

    这实际上确实对编译器中的优化器产生了(潜在的)巨大的差异。编译器通过函数定义后的空throw()语句以及适当的扩展,实际上已经有了这个特性很多年了。我可以向您保证,现代编译器确实利用了这些知识来生成更好的代码。

    几乎编译器中的每一个优化都使用一个叫做函数“流图”的东西来推理什么是合法的。流图由通常称为函数的“块”(具有单个入口和单个出口的代码区域)和块之间的边组成,这些边指示流可以跳到哪里。Noexcept更改流图。

    你要求一个具体的例子。请考虑以下代码:

    void foo(int x) {
        try {
            bar();
            x = 5;
            // Other stuff which doesn't modify x, but might throw
        } catch(...) {
            // Don't modify x
        }
    
        baz(x); // Or other statement using x
    }
    

    如果bar被标记为noexcept(执行无法在bar的末尾和catch语句之间跳转),则该函数的流图会有所不同。当标记为noexcept时,编译器确定在baz函数期间x的值是5-x=5块被称为“支配”baz(x)块,而没有从bar()到catch语句的边缘。

    然后它可以做一些叫做“恒定传播”的事情来生成更高效的代码。在这里,如果baz是内联的,那么使用x的语句也可能包含常量,然后可以将以前的运行时计算转换为编译时计算,等等。

    总之,简短的答案:noexcept让编译器生成一个更紧密的流图,流图用于推理各种常见的编译器优化。对于编译器来说,这种性质的用户注释是非常棒的。编译器会试图弄清楚这些东西,但它通常做不到(问题函数可能在编译器不可见的另一个对象文件中,或者可传递地使用一些不可见的函数),或者当它弄清楚时,可能会抛出一些您甚至没有意识到的琐碎异常,所以它不能隐式地将其标记为noexcept(例如,分配内存可能会抛出bad_alloc)。