在上一节关于错误处理中,我们讨论了如何利用 assert()
、std::cerr
和 exit()
来处理错误。然而,我们当时把“异常”这一主题留待本节深入探讨。
当返回码失效时
在编写可复用代码时,错误处理不可或缺。最常见的潜在错误处理方式之一便是通过返回码。例如:
#include <string_view>
int findFirstChar(std::string_view string, char ch)
{
// 依次遍历 string 中的每个字符
for (std::size_t index{ 0 }; index < string.length(); ++index)
// 若字符匹配 ch,则返回其索引
if (string[index] == ch)
return index;
// 若无匹配,则返回 -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 {}; // 现在必须传入一个 bool 以判断是否成功
double result { divide(5, 3, success) };
if (!success) // 使用前必须检查
std::cerr << "发生错误" << std::endl;
else
std::cout << "结果是 " << result << '\n';
}
- 在可能出现多种错误的连续代码中,错误码必须频频检查。以下示例展示了解析文本文件并提取若干预期值的代码片段:
std::ifstream setupIni { "setup.ini" }; // 打开 setup.ini
// 若文件无法打开(例如缺失),则返回某个错误枚举
if (!setupIni)
return ERROR_OPENING_FILE;
// 接下来从文件中读取一系列值
if (!readIntegerFromFile(setupIni, m_firstParameter)) // 尝试读取一个整数
return ERROR_READING_VALUE; // 返回枚举值指示读取失败
if (!readDoubleFromFile(setupIni, m_secondParameter)) // 尝试读取一个 double
return ERROR_READING_VALUE;
if (!readFloatFromFile(setupIni, m_thirdParameter)) // 尝试读取一个 float
return ERROR_READING_VALUE;
我们尚未涉及文件访问,故无需纠结上述细节,只需注意:每次调用都必须进行错误检查并返回调用者。若需读取的参数多达二十个且类型各异,则需重复检查并返回 ERROR_READING_VALUE
二十次!如此频繁的错误检查与返回值使得函数原本意图愈发难辨。
返回码与构造函数难以兼容。若在对象构造过程中出现灾难性错误,而构造函数既无返回类型传递状态,又难以通过引用参数优雅地返回错误;即便强行如此,对象仍会被创建,后续必须另行处置或销毁。
当错误码返回给调用者时,调用者未必具备处理该错误的能力。若调用者不愿处理,则要么忽略(错误随之永久丢失),要么将错误继续向上传递。此过程繁琐,并可能重现前述诸多问题。
综上所述,返回码的核心弊端在于:错误处理代码与正常控制流紧密耦合,既限制了代码结构,也限制了合理的错误处理方式。
异常
异常处理机制提供了一种途径,可将错误或其他异常情形的处理与常规控制流解耦。这使开发者能够依据实际需求,在恰当的时间与位置灵活处理错误,从而消除(或极大缓解)返回码所带来的混乱。
接下来,我们将探讨 C++ 中异常的运作机制。