item 14: accomplish

This commit is contained in:
racaljk 2018-11-01 10:57:54 +08:00
parent e28ac91b39
commit c1ca31f546

View File

@ -70,3 +70,52 @@ struct pair {
请注意我说的那些很自然不应该抛异常的函数实现。为了**noexcept**而扭曲函数实现达成目的是本末倒置。是把马放到马车前,是一叶障目不见泰山。是...选择你喜欢的比喻吧。如果一个简单的函数实现可能引发异常(即调用它可能抛出异常),而你为了讨好调用者隐藏了这个(即捕获所有异常,然后替换为状态码或者特殊返回值),这不仅会使你的函数实现变得复杂,还会让所有调用点的代码变得复杂。调用者可能不得不检查状态码或特殊返回值。而这些**复杂**的运行时开销(额外的分支,大的函数放入指令缓存)可以超出**noexcept**带来的性能提升,再加上你会悲哀的发现这些代码又难读又难维护。那是糟糕的软件工程化。
对于一些函数,使其成为**noexcept**是很重要的它们应当默认如是。在C++98构造函数和析构函数抛出异常是糟糕的代码设计——不管是用户定义的还是编译器生成的构造析构都是**noexcept**。因此它们不需要声明**noexcept**。(这么做也不会有问题,只是不合常规)。析构函数非隐式**noexcept**的情况仅当类的数据成员明确声明它的析构函数可能抛出异常(即,声明`noexcept(false)`)。这种析构函数不常见,标准库里面没有。如果一个对象的析构函数可能被标准库使用,析构函数又可能抛异常,那么程序的行为是未定义的。
值得注意的是一些库接口设计者会区分有宽泛契约(**wild contracts**)和严格契约(**narrow contracts**)的函数。有宽泛契约的函数没有前置条件。这种函数不管程序状态如何都能调用,它对调用者传来的实参不设约束。宽泛契约的函数决不表现出未定义行为。
反之,没有宽泛契约的函数就有严格契约。对于这些函数,如果违反前置条件,结果将会是未定义的。
如果你写了一个有宽泛契约的函数并且你知道它不会抛异常,那么遵循这个条款给它声明一个**noexcept** 是很容易的。
对于严格契约的函数,情况就有点微妙了。举个例子,假如你在写一个参数为**std::string**的函数**f**,并且这个函
数f很自然的决不引发异常。这就在建议我们**f**应该被声明为**noexcept** 。
现在假如**f**有一个前置条件:类型为**std::string**的参数的长度不能超过32个字符。如果现在调用f并传给它一个
大于32字符的参数函数行为将是未定义的因为违反了 _口头/文档定义的_ 前置条件,导致了未定义行为。**f**没有
义务去检查前置条件它假设这些前置条件都是满足的。调用者有责任确保参数字符不超过32字符等这些假设有效。
即使有前置条件,将**f**声明为**noexcept**似乎也是合适的:
```cpp
void f(const std::string& s) noexcept; // 前置条件:
// s.length() <= 32
```
**f**的实现者决定在函数里面检查前置条件冲突。虽然检查是没有必要的,但是也没禁止这么做。另外在系统测试时,检查
前置条件可能就是有用的了。debug一个抛出的异常一般都比跟踪未定义行为起因更容易。那么怎么报告前置条件冲突使得
测试工具或客户端错误处理程序能检测到它呢?简单直接的做法是抛出`"precondition was violated"`异常,但是如果
f声明了**noexcept**,这就行不通了;抛出一个异常会导致程序终止。因为这个原因,区分严格/宽泛契约库设计者一般
会将**noexcept**留给宽泛契约函数。
作为结束语,让我详细说明一下之前的观察,即编译器不会为函数实现和异常规范提供一致性保障。考虑下面的代码,它是
完全正确的:
```cpp
void setup(); // 函数定义另在一处
void cleanup();
void doWork() noexcept
{
setup(); // 前置设置
… // 真实工作
cleanup(); // 执行后置清理
}
```
这里,**doWork**声明为**noexcept**,即使它调用了非**noexcept**函数`setup`和`cleanup`。看起来有点矛盾,
其实可以猜想`setup`和`cleanup`在文档上写明了它们决不抛出异常,即使它们没有写上**noexcept**。至于为什么
明明不抛异常却不写**noexcept**也是有合理原因的。比如它们可能是用C写的库函数的一部分。即使一些函数从
C标准库移动到了**std**命名空间,也可能缺少异常规范,**std::strlen**就是一个例子,它没有声明**noexcept**)。
或者它们可能是C++98库的一部分它们不使用C++98异常规范的函数的一部分到了C++11还没有修订。
因为有很多合理原因解释为什么**noexcept**依赖于缺少**noexcept**保证的函数所以C++允许这些代码,编译器
一般也不会给出warnigns。
记住:
+ **noexcept**是函数接口的一部分,这意味着调用者会依赖它、
+ **noexcept**函数较之于非**noexcept**函数更容易优化
+ **noexcept**对于移动语义,**swap**,内存释放函数和析构函数非常有用
+ 大多数函数是异常中立的(译注:可能抛也可能不抛异常)而不是**noexcept**