设为首页 加入收藏

TOP

Item 14: 如果函数不会抛出异常就把它们声明为noexcept(二)
2017-10-12 10:55:41 】 浏览:7472
Tags:Item 14: 如果 函数 不会 异常 它们 声明 noexcept
oid swap(pair& p) noexcept(noexcept(swap(first, p.first)) && noexcept(swap(second, p.second))); ... };

这些函数是条件noexcept(conditionally noexcept):它们是否是noexcept取决于noexcept中的表达式是否是noexcept。举个例子,给出两个Widget的数组,只有用数组中的元素来调用的swap是noexcept时(也就是用Widget来调用的swap是noexcept时),用数组调用的swap才是noexcept。反过来,这也决定了Widget的二维数组是否是noexcept。相似地,std::pair 对象的swap成员函数是否是noexcept取决于用Widget调用的swap是否是noexcept。事实上,只有低层次数据结构的swap调用是noexcept,才能使得高层次数据结构的swap调用是noexcept。这鼓励你尽量提供noexcept swap函数。

现在我希望你已经对noexcept提供的优化机会感到兴奋了。哎,可是我必须浇灭你的热情。优化很重要,但是正确性更重要。我记得在这个Item的开始说过,noexcept是函数接口的一部分,所以只有当你愿意长期致力于noexcept的实现时,你才应该声明函数为noexcept。如果你声明一个函数为noexcept,并且之后对于这个决定后悔了,你的选择是将是绝望的。1:你能把noexcept从函数声明中移除(也就是改变函数接口),则客户代码会遭受运行期风险。2:你也能改变函数的实现,让异常能够逃离函数,但是保持着原来的异常规范(现在,原来的规范声明是错误的)。如果你这么做,当一个异常尝试逃离函数时,你的程序将会终止。3:或者你可以抛弃一开始想要改变实现的想法,回归到你现存的实现中去。这些选择没有一个是好的选择。

事实上,很多函数都是异常中立的(exception-neutral)。这些函数自己不抛出异常,但是他们调用的函数可能抛出异常。当发生这样的事时,异常中立的函数允许异常通过调用链传给处理程序。异常中立的函数永远不是noexcept,因为他们可能抛出“我只经过一下”(异常产生的地方在别的函数中,但是需要经过我们来传递出去)的异常。因此,很大部分函数都不适合设计为noexcept。

然而,一些函数天生就不抛出异常,并且对于一些函数(特别是move操作和swap函数)成为noexcept能有很大的回报,只要有任何可能,它们都值得实现为noexcept。当你能很明确地说一个函数永远不应该抛出异常的时候,你应该明确地把这个函数声明为noexcept。

请记住,我说过一些函数天生就适合实现为noexcept。但是如果扭曲一个函数的实现来允许noexcept声明,这样是本末倒置的。假设一个简单的函数实现可能会产生异常(比如,它调用的函数可能抛出异常),如果你想隐藏这样的调用(比如,在函数内部捕捉所有的异常并且把它们替换成相应的状态值或者特殊的返回值)不仅将使你的函数实现更加复杂,它还将使你的函数调用变得更加复杂。举个例子,调用者必须要检查状态值或特殊的返回值。同时增加的运行期的费用(比如,额外的分支,以及更大的函数在指令缓存上会增加更大的压力。等等)会超过你希望通过noexcept来实现的加速,同时,你还要承担源代码更加难以理解和维护的负担。这真是一个糟糕的软件工程。

对于一些函数来说,声明为noexcept不是如此重要,它们在默认情况下就是noexcept了。在C++98中,允许内存释放函数(比如operator delete和operator delete[])和析构函数抛出异常是很糟糕的设计风格,在C++11中,这种设计风格已经在语言规则的层次上得到了改进。默认情况下,所有的内存释放函数和所有的析构函数(包括用户自定义的和编译器自动生成的)都隐式成为noexcept。因此我们不需要把它们声明成noexcept的(这么做不会造成任何问题,但是不寻常。)只有一种情况析构函数不是隐式noexcept,就是当类中的一个成员变量(包括继承来和被包含在成员变量中的成员变量)的析构函数声明表示了它可能会抛出异常(比如,声明这个析构函数为“noexcept(false)”)。这样的声明是不寻常的,标准库中就没有。如果把一个带有能抛出异常的析构函数的对象用在标准库中(比如,这个对象在一个容器中或者这个对象被传给一个算法),那么程序的行为是未定义的。

我们值得去注意一些库的接口设计区分了宽接口(wide contract)和窄接口(narrow contract)。一个带宽接口的函数没有前提条件。这样的函数被调用时不需要注意程序的状态,它在传入的参数方面没有限制。带宽接口的函数永远不会展现未定义行为。

不带宽接口条件的函数就是窄接口函数。对这些函数来说,如果传入的参数违反了前提条件,结果将是未定义的。

如果你在写一个宽接口的函数,并且你知道你不会抛出一个异常,那就遵循本Item的建议,把它声明为noexcept。对于那些窄接口的函数,情况将变得很棘手。举个例子,假设你正在写一个函数f,这个函数接受一个std::string参数,并且它假设f的实现永远不会产生一个异常。这个假设建议我们把f声明为noexcept。

现在假设f有一个前提条件:std::string参数的数据长度不会超过32个字节。如果用一个超过32字节的std::string来调用f,f的行为将是未定义的,因为一个不符合前提条件的参数会导致未定义行为。f没有义务去检查前提条件,因为函数假设它们的前提条件是被满足的(调用者有责任确保这些假设是有效的)。由于前提条件的存在,把f声明为noexcept看起来是合理的。

void f(const std::string& s) noexcept;      //前提条件:s.length() <= 32

但是假设f的实现选择检查前提条件是否被违反了。检查本不是必须的,但是它也不是被禁止的,并且检查一下前提条件是有用的(比如,在进行系统测试的时候)。调试时,捕捉一个抛出的异常总是比尝试找出未定义行为的原因要简单很多。但是要怎么报道出前提条件被违反了呢?只有报道了才能让测试工具或客户端的错误处理机制来捕捉到它。一个直接的方法就是抛出一个“前提条件被违反”的异常,但是如果f被声明为noexcept,那么这个方法就不可行了,抛出一个异常就会导致程序终止。因此,区分宽接口和窄接口的库设计者通常只为宽接口函数提供noexcept声明。

最后还有一点,让我完善一下我之前的观点(编译器常常无法对“找出函数实现和它们的异常规范之间的矛盾”提供帮助)。考虑一下下面的代码,这段代码是完全合法的:

void setup();           //在别处定义的函数
void cleanup();         

void doWork() noexcept
{
    setup();            //做设置工作

    ...                 //做实际的工作

    cleanup();          //做清理工作
}

在这里,尽管doWork调用了non-noexcept函数(setup和cleanup),doWork还是被声明为no

首页 上一页 1 2 3 下一页 尾页 2/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇路径搜索——网络寻路 下一篇Item 14: 如果函数不会抛出异常就..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目