现代 C++ 异常机制今非昔比,莫要沉迷错误码!「上」

以下内容为本人的烂笔头,如需要转载,请声明原文链接 微信公众号「ENG八戒」mp.weixin.qq.com/s/ksxMkTmmW...

错误码是古董玩意?

习惯写 C 的朋友可能比较习惯使用错误码来携带异常信息,返回错误码作为错误处理的判断条件。错误码也有好几种形式,比如函数返回值、输出参数或者非局部变量等都可以作为错误码的存储手段。自定义函数可以通过返回值、输出参数等包含异常信息,而标准库函数则会通过覆盖全局变量 errno 的值来传递异常信息。

这种利用错误码的异常处理方式在嵌入式领域尤为常见,那么它适合所有场景吗?它有什么缺点?

最明显的不足就是利用错误码的方式会导致代码结构混乱而复杂。

下面让我们看看普通的文件读取功能示例的代码

arduino 复制代码
#include <errno.h>
#include <string.h>
#include <iostream>
using namespace std;
void read()
{
    FILE *file = fopen("./file.txt", "r");
    if (file == NULL) {
        // 错误处理
        cout << "Err opening file: "
            << strerror(errno) << "\n";
    } else {
        char buffer[1024];
        size_t read_bytes = fread(buffer,
                                1,
                                sizeof(buffer),
                                file);
        if (read_bytes != sizeof(buffer)) {
            // 错误处理
            cout << "Err reading file: "
                << strerror(errno) << "\n";
        } else {
            // 正常处理读取到的数据
            // ...
        }
        
        if (fclose(file) != 0) {
            // 错误处理
            cout << "Err closing file: "
                << strerror(errno) << "\n";
        }
    }
}

上面的代码中调用了标准库中的 fopen、fread 和 fclose 等函数来操作文件,并通过函数返回值和全局变量 errno 来检查错误。代码比较简单,仅仅是过程式的结构,目前看起来是运行正常的,异常也可以被正确处理。

使用返回值来指示函数执行成功与否及其结果时,会引入很多的挑战。选择一个错误返回值时,开发者需要精心设计,以确保它不会与有效返回值混淆。比如返回一个整形数值,0 既可以是正常的计算结果,也可以是错误码。

虽然可以通过返回组合数据解决有效返回值和错误码之间容易混淆的问题,但是设计应该是简单而美观的。

如此,随着代码的扩充和复杂度提高,后续代码维护就会越来越难和令到开发人员痛苦不堪,至少笔者我认为上面的示例代码有几点不足:

  1. 代码耦合度过高:正常业务流程代码和错误处理代码交织在一起,使得代码整体结构不清晰,职责不明确,也不利于阅读和维护。

  2. 错误处理分支分散:随着文件操作增多,每一个步骤都可能产生错误,这就意味着会有更多的条件 if 语句用于检查错误码 errno 并且是散落在各个文件操作步骤中,最终代码会越来越庞大且不易梳理。

  3. 潜在的遗漏处理:如果在后续的开发过程中,添加了新的文件操作而忘记添加相应的错误检查,可能会导致一些错误未被处理,削弱了程序的健壮性。

  4. 可读性和可扩展性差:过多和分散的错误处理代码会降低代码的可读性和可扩展性,特别是代码流程控制比较复杂时,阅读和理解代码的成本会显著增加,无疑增加了开发人员负担和接手门槛。

上面说的是过程式的设计,再说使用面向对象的开发方法,在类的构造函数中经常会申请内存、文件流、socket 等资源,如果调用类构造函数过程中发生错误,那么继续单纯通过返回错误码就很难判断异常的发生。一旦有错误情况被忽略,就会导致后续的操作都可能失败,甚至导致资源泄漏等严重问题。比如

c 复制代码
std::vector<double> v(100);
ofstream os("myfile.txt");

这段代码调用了 std::vector 和 ofstream 两个类来实例化对象,实例化时会调用各自的类构造函数,如果构造函数执行失败报错,这时是无法通过返回错误码来判断错误情况的,还必须搭配其它的查询接口。比如

c 复制代码
std::vector<double> v(100);
if (v.initial_fail()) {
    // 错误处理
}
ofstream os("myfile.txt");
if (os.initial_fail()) {
    // 错误处理
}

各对象的接口 initial_fail() 不是必须的,但为了配合错误码的使用,应该有类似的构造失败查询接口。这里的示例代码只有简单的几个对象,还能正常运行(包括处理异常情况),如果对象比较复杂甚至相互嵌套,异常处理的代码部分显然也会越来越复杂。

在构造函数中申请资源,然后在析构函数中释放资源,也就是我们常说的 RAII,这是现代 C++ 最高效的设计技术基础。所以,在类的构造函数中申请资源的动作会应用得非常普遍,那么除了可以使用错误码之外,势必还应该有其它高效的异常处理方式。(真爱生命,码农的命也是命,怎么能浪费在敲代码上面呢?)

关于 RAII 的介绍,这里不展开了,可以看看笔者八戒之前写的文章 《都说 C++ 没有 GC,RAII: 那么我算个啥?》。

再者,在多线程的业务开发中,错误码是不安全的,必须配合同步措施,比如锁、条件变量、信号量等等,要想用好错误码需要投入非常多的心思。

可见,依赖返回错误码来判断异常,这种异常处理方式已经非常落后。


全文未结束,本文只是上篇。如果各位同学朋友有什么疑问可以联系笔者,当然笔者也愿意和你进一步探讨这方面的问题。另外,八戒有自己的技术圈朋友群,如果读者朋友想进群交流技术问题,欢迎联系我。下拉到文章顶部有我的联系方式!

最后,非常感激各位朋友的点 「赞」 和点击 「在看」,谢谢!

相关推荐
怀澈1221 小时前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
chnming19872 小时前
STL关联式容器之set
开发语言·c++
威桑2 小时前
MinGW 与 MSVC 的区别与联系及相关特性分析
c++·mingw·msvc
熬夜学编程的小王2 小时前
【C++篇】深度解析 C++ List 容器:底层设计与实现揭秘
开发语言·数据结构·c++·stl·list
yigan_Eins2 小时前
【数论】莫比乌斯函数及其反演
c++·经验分享·算法
Mr.132 小时前
什么是 C++ 中的初始化列表?它的作用是什么?初始化列表和在构造函数体内赋值有什么区别?
开发语言·c++
阿史大杯茶2 小时前
AtCoder Beginner Contest 381(ABCDEF 题)视频讲解
数据结构·c++·算法
C++忠实粉丝2 小时前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
我们的五年3 小时前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++
程序猿阿伟3 小时前
《C++ 实现区块链:区块时间戳的存储与验证机制解析》
开发语言·c++·区块链