27-1 异常情况的必要性
在上一节关于错误处理的课程中,我们讨论了如何使用 assert()、std::cerr 和 exit() 来处理错误。但是,我们推迟了一个主题,现在我们将讨论这个主题:异常。
返回码失败
编写可重用代码时,错误处理必不可少。处理潜在错误最常见的方法之一是使用返回码。例如:
#include <string_view>
int findFirstChar(std::string_view string, char ch)
{
// Step through each character in string
for (std::size_t index{ 0 }; index < string.length(); ++index)
// If the character matches ch, return its index
if (string[index] == ch)
return index;
// If no match was found, return -1
return -1;
}

此函数返回字符串中第一个与字符 ch 匹配的字符的索引。如果找不到该字符,则函数返回 -1,表示未找到该字符。
这种方法的主要优点是极其简单。然而,使用返回码也存在一些缺点,在处理复杂情况时这些缺点会很快显现出来:
首先,返回值可能很晦涩难懂——如果一个函数返回 -1,它是在表示错误,还是实际上就是一个有效的返回值?如果不深入研究函数内部代码或查阅文档,通常很难判断。
其次,函数只能返回一个值,那么当你需要同时返回函数结果和可能的错误代码时该怎么办呢?考虑以下函数:
double divide(int x, int y)
{
return static_cast<double>(x)/y;
}
这个函数急需一些错误处理,因为如果用户将参数 y 设置为 0,它就会崩溃。然而,它还需要返回 x/y 的结果。如何才能同时做到这两点呢?最常见的答案是,结果或错误处理必须作为引用参数返回,但这会导致代码臃肿且使用不便。例如:
#include <iostream>
double divide(int x, int y, bool& outSuccess)
{
if (y == 0)
{
outSuccess = false;
return 0.0;
}
outSuccess = true;
return static_cast<double>(x)/y;
}
int main()
{
bool success {}; // we must now pass in a bool value to see if the call was successful
double result { divide(5, 3, success) };
if (!success) // and check it before we use the result
std::cerr << "An error occurred" << std::endl;
else
std::cout << "The answer is " << result << '\n';
}

第三,在可能出现很多错误的程序代码序列中,必须不断检查错误代码。例如,以下代码片段用于解析文本文件,查找其中应该存在的值:
std::ifstream setupIni { "setup.ini" }; // open setup.ini for reading
// If the file couldn't be opened (e.g. because it was missing) return some error enum
if (!setupIni)
return ERROR_OPENING_FILE;
// Now read a bunch of values from a file
if (!readIntegerFromFile(setupIni, m_firstParameter)) // try to read an integer from the file
return ERROR_READING_VALUE; // Return enum value indicating value couldn't be read
if (!readDoubleFromFile(setupIni, m_secondParameter)) // try to read a double from the file
return ERROR_READING_VALUE;
if (!readFloatFromFile(setupIni, m_thirdParameter)) // try to read a float from the file
return ERROR_READING_VALUE;
我们还没讲到文件访问,所以如果你不理解上面的代码是怎么运作的,也不用担心——只需记住,每次调用都需要进行错误检查并返回结果给调用者。现在想象一下,如果有二十个不同类型的参数——你实际上要检查错误并返回二十次 ERROR_READING_VALUE!所有这些错误检查和返回值使得判断函数到底想做什么变得更加困难。
第四,返回码与构造函数并不兼容。如果您在创建对象时,构造函数内部发生灾难性错误,会发生什么?构造函数没有返回类型来传递状态指示符,而通过引用参数传递状态指示符既繁琐又必须显式检查。此外,即使您这样做,对象仍然会被创建,然后仍然需要进行处理或释放。
最后,当调用者收到错误代码时,它未必总能妥善处理该错误。如果调用者不想处理该错误,要么只能忽略它(这样错误信息将永远丢失),要么只能将错误信息向上返回给调用它的函数。这样做可能会很麻烦,并导致许多与上述相同的问题。
总而言之,返回码的主要问题在于,错误处理代码最终与代码的正常控制流紧密相连。这反过来又限制了代码的布局方式以及错误处理的合理性。
例外情况
异常处理提供了一种机制,可以将错误或其他异常情况的处理与代码的常规控制流程解耦。这使得我们可以更灵活地根据具体情况选择最合适的错误处理时间和方式,从而减轻(如果不是全部)返回码带来的混乱。
下一课,我们将了解 C++ 中的异常是如何工作的。

浙公网安备 33010602011771号