Error Detection and Handling¶
Unit Test¶
单独测试一小部分代码以确保代码“单元”正确,称为单元测试。每个单元测试旨在确保单元的特定行为是正确的。
以小的、定义良好的单元(函数或类)编写程序,经常编译,并随时测试代码。
如何写单元测试?
不同的语言有不同的测试框架,我们暂时先忽略这一块
函数错误处理¶
这里我们讨论==函数内部的错误处理==策略(出现问题时该怎么做),至于检验用户输入是否合法,我们留给后面介绍
策略:
- Handling errors within the function
- Pass the error back to the caller to deal with
- Halt the program
- Throw an exception
Handling errors within the function¶
如果可能,最好的策略是在发生错误的同一函数中从错误中恢复,以便可以包含并纠正错误,而不会影响函数外部的任何代码。
这里有两个选项:重试直到成功,或者取消正在执行的操作。
1)重试直到成功:
比如:如果程序需要互联网连接,而用户失去了连接,则程序可能会显示警告,然后使用循环定期重新检查互联网连接
2)忽略错误和/或取消操作:
C++ | |
---|---|
1 2 3 4 5 6 |
|
修改后:
C++ | |
---|---|
1 2 3 4 5 6 7 |
|
当然,这只是显示地爆出了错误,并没有起到修正的作用;如果调用函数期望被调用函数产生返回值或一些有用的副作用,那么仅仅忽略错误可能不是一个 good choice.
Pass the error back to the caller to deal with¶
在许多情况下,检测到错误的函数中无法合理地处理错误,我们有时候希望最好能将错误传递回调用函数,让调用函数决定如何处理错误。
我们通常采用 return value
来反馈!
C++ | |
---|---|
1 2 3 4 5 6 7 |
|
这样是可以打印出错误情况,但是我们可能不仅仅需要知道,还得handle它,因此我们改变函数的返回值类型
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
这样我们就可以在调用函数中处理错误情况了,比如在修改后的函数中,我们可以用handle()
函数来进行错误处理:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
但是!如果函数返回一个正常值,事情就有点复杂了。
在某些情况下,不会使用完整范围的返回值。在这种情况下,我们可以使用通常不可能出现的返回值来指示错误。例如,考虑以下函数:
C++ | |
---|---|
1 2 3 4 5 |
|
如果用户将此函数调用为reciprocal(0.0)
会发生什么情况?我们遇到divide by zero
错误和程序崩溃,因此显然我们应该防止这种情况。
但是这个函数必须返回一个double
值,那么我们应该返回什么值呢?事实证明,这个函数永远不会产生0.0
作为合法结果,因此我们可以返回0.0
来指示错误情况。
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
哨兵值
哨兵值:是在函数或算法上下文中具有某些特殊含义的值
比如这里的error_no_reciprocal,它的值是0.0,这就是一个哨兵值,指示函数失败。
调用者可以测试返回值以查看它是否与哨兵值匹配 - 如果是,则调用者知道函数失败
如果函数可以生成完整范围的返回值,则不存在哨兵值
函数可以生成完整范围的返回值,哨兵值不存在,这该咋办?
这种情况下,返回std::optional
(或std::expected
)将是更好的选择!
Fatal errors¶
如果错误严重到程序无法继续正常运行,则称为不可恢复错误(也称为fatal error
)。在这种情况下,最好的办法是终止该程序。
如果代码位于main()
或直接从main()
调用的函数中,最好的办法是让main()
返回非零状态代码。但是,如果我们深入某些嵌套子函数,则可能不方便或不可能将错误一直传播回main()
。
在这种情况下,可以使用halt statement (例如std::exit()
)
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
这里我们需要区分:exit(1)
和return 1
的区别:
return
将控制权返回给调用函数,允许程序继续执行调用栈的展开std::exit()
直接终止整个程序,不会返回到调用函数,后面的代码都不会执行
当需要从深层嵌套函数立即终止程序时,使用std::exit()
更方便; 如果需要正常的清理过程,应该使用return
并将错误状态传播回main()
.
在main()函数中,return 0
和 exit(0)
两者效果基本相同,因为都会导致程序终止。推荐使用return
。
Exceptions¶
C++ 提供了一种完全独立的方法将错误传递回调用者: exceptions
.
基本思想是,当发生错误时,“抛出”异常。如果当前函数没有“捕获”错误,则函数的调用者有机会捕获错误。如果调用者没有捕获错误,则调用者的调用者有机会捕获错误。错误在调用堆栈中逐渐向上移动,直到它被捕获并处理(此时执行正常继续),或者直到 main() 无法处理错误(此时程序因异常错误而终止)。
将底层的错误逐级上传,在这一过程中能handle最好,真hold不住的话,那就在最顶层的main
函数报错
断言¶
Assert
是一种在==程序运行时==检查程序状态的方法。如果断言的条件为假,则程序会终止。assert
通常用于检查不应该发生的错误。
如果表达式的计算结果为true
,则断言语句不执行任何操作。如果条件表达式的计算结果为false
,则会显示错误消息并终止程序(通过std::abort
)。
此错误消息通常包含失败的文本表达式,以及代码文件的名称和断言的行号。这使得不仅可以很容易地判断出问题所在,还可以轻松判断代码中问题发生的位置。这可以极大地帮助调试工作。
最初形式:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
在 C++ 中,运行时assert
是通过断言预处理器宏实现的,该宏位于标头:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
当程序调用 calculateTimeUntilObjectHitsGround(100.0, -9.8)
时,assert(gravity > 0.0)
将计算为false
,这将触发断言,并将打印类似如下的消息:
C++ | |
---|---|
1 |
|
不一定完全一致,实际消息因使用的编译器而异。
采用更具有描述性的断言语句更具描述性¶
有时断言表达式的描述性不强。考虑以下陈述:
C++ | |
---|---|
1 |
|
幸运的是,您可以使用一个小技巧来使您的断言语句更具描述性。只需添加一个由逻辑 AND
连接的字符串文字:
C++ | |
---|---|
1 |
|
这样,当断言触发时,字符串文字将包含在断言消息中:
C++ | |
---|---|
1 |
|
如何我们可以获取一些额外背景信息。
想把assert关了怎么办¶
每次检查断言条件时,assert
宏都会产生少量性能成本。此外,断言(理想情况下)不应在生产代码中遇到(因为您的代码应该已经经过彻底测试)。
因此,大多数开发人员更喜欢断言仅在调试版本中有效。 C++ 提供了一种内置方法来关闭生产代码中的断言:如果定义了预处理器宏NDEBUG
,则断言宏将被禁用。
放在自己的行上的任何 #include
之前: #define NDEBUG
(以禁用断言)或 #undef NDEBUG
(以启用断言)。
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
std::optional¶
C++17 引入了 std::optional
,它是实现可选值的类模板类型。也就是说,std::optional<T>
可以具有 T
类型的值,也可以没有。我们可以用它来实现上面的第三个选项:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
显示结果:
C++ | |
---|---|
1 2 |
|
初始化
C++ | |
---|---|
1 2 3 |
|
查看std::optional
是否有值
C++ | |
---|---|
1 2 3 4 5 6 7 |
|
从std::optional
获取值
C++ | |
---|---|
1 2 |
|
工作原理
我们的doIntDivision()
现在返回std::optional<int>
而不是int
。
在函数体内,如果我们检测到错误,我们将返回{}
,它隐式返回一个不包含任何值的std::optional
。如果我们有一个值,我们会返回该值,这会隐式返回包含该值的std::optional
,如: {valueXXX}
。
建议仅当T
通常==按值传递==时才使用std::optional<T>
作为可选参数。否则,使用const T*
.