使用 C++ 处理错误
本章将聚焦于 C++ 中的错误处理。作为一名程序员,你不可避免地会遇到需要确定如何传播程序错误的情况。无论是使用错误代码还是异常,我们都将深入了解它们,以更好地理解如何有效使用它们。
在这一章中,我们将探讨如何使用 C++ 处理 POSIX API 报告的错误。我们将从介绍 errno
线程局部变量和 strerror
函数开始。之后,我们将引入 std::error_code
和 std::error_condition
,并演示它们如何帮助封装来自 POSIX API 的 POSIX 错误。我们还将研究自定义错误类别,这允许我们比较不同来源产生的错误,并开发平台独立的错误处理代码。
随着学习的深入,我们将了解 C++ 中的异常以及如何将 std::error_code
转换为 std::system_error
异常。我们还将探索处理异常的一些最佳实践,例如通过值抛出异常并通过引用捕获它们。此外,我们将了解对象切片,这是在我们通过值而不是引用捕获异常时可能发生的副作用。最后,我们将深入了解 C++ 中的 RAII 技术,它消除了该语言中 finally
构造的需求。
通过本章的学习,你将对 C++ 中处理错误的各种方式有一个全面的了解,并将熟悉几种创建抗错误代码的技术。
总结起来,我们将涵盖以下主题:
- 使用 C++ 处理 POSIX API 的错误
- 从错误代码到异常
好的,我们开始吧!
技术要求
本章中的所有示例都在以下配置的环境中进行了测试:
- Linux Mint 21 Cinnamon 版本
- GCC 12.2,编译器标志:
-std=c++20
- 稳定的互联网连接
- 请确保您的环境至少是这么新。对于所有示例,您还可以选择使用 https://godbolt.org/。
使用 C++ 处理 POSIX API 的错误
在遵循 POSIX 标准的系统中,如 Unix 和 Linux,错误处理基于使用错误代码和错误消息在函数和应用程序之间传递错误。
通常情况下,当函数遇到错误时,它会返回一个非零错误代码,并将 errno
全局变量设置为特定的错误值,以指示错误的性质。然后,应用程序可以使用 errno
变量来确定错误的原因,并采取适当的行动。
除了错误代码,遵循 POSIX 标准的函数通常还提供描述错误性质的详细错误消息。这些错误消息通常是通过 strerror
函数访问的,该函数接受一个错误代码作为输入,并返回一个以空字符结尾的字符序列指针,其中包含相应的错误消息。
POSIX 错误处理风格要求开发人员在每次可能失败的系统调用或函数调用后检查错误,并以一致且有意义的方式处理错误。这可以包括记录错误消息、重试失败的操作,或在发生严重错误时终止程序。
让我们看一个示例,我们在其中演示如何使用 errno
变量和 strerror()
函数来处理 C++ 中的 POSIX 函数错误。
该示例使用了 open()
和 close()
POSIX 函数,它们尝试在我们的 Linux 测试环境的文件系统中打开和关闭文件:
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
int main() {
const int fd{open("no-such-file.txt", O_RDONLY)}; //{1}
if (fd == -1) {
std::cerr << "Error opening file: " <<
strerror(errno) << '\n';
std::cerr << "Error code: " << errno << '\n';
return EXIT_FAILURE;
}
// Do something with the file...
if (close(fd) == -1) {
std::cerr << "Error closing file: " <<
strerror(errno) << '\n';
std::cerr << "Error code: " << errno << '\n';
return EXIT_FAILURE;
}
return 0;
}
在这个示例中,我们尝试使用 open()
函数打开一个名为 no-such-file.txt
的文件进行读取;见标记 {1}
。如果成功,open()
返回一个非负整数,对应于成功打开文件的文件描述符 ID。如果 open()
返回 -1
,我们知道发生了错误,所以我们使用 strerror(errno)
打印错误消息,并返回 errno
的值,其中写有相应的错误代码。
如果 open()
成功,我们对文件进行一些操作,然后使用 close()
函数关闭它。如果 close()
返回 -1
,我们再次使用 strerror(errno)
打印错误消息,并返回 errno
的值。
这是处理 POSIX 函数错误的常见技术。在出现错误的情况下,它们返回 -1
并将相应的错误代码设置到 errno
变量中。errno
变量是 int
类型的 线程局部 可修改变量。这意味着你可以在多线程环境中安全使用它。每个线程将有自己的副本,此线程调用的 POSIX 方法将使用此实例来报告错误。
为了在出现错误时打印有意义的消息,我们使用了 strerror()
函数,它接受一个整数并尝试将其值与系统特定错误代码的众所周知的描述列表相匹配。open()
函数可以报告几种错误,并根据发生的错误类型设置 errno
的不同值。让我们看看示例的输出:
Error opening file: No such file or directory
Error code: 2
正如我们所见,open()
方法未能打开文件,因为它不存在。在这种情况下,它将 errno
设置为 2
的值,对应于函数文档中指定的 ENOENT
值。在进行系统调用之前显式将 errno
设置为 0
是一个好习惯,以确保在调用后,你可以读取其真实响应。
使用 std::error_code 和 std::error_condition
在C++标准库中,提供了多个类用于处理来自低级API(例如POSIX接口)的错误。这些类包括std::error_code
,用于处理特定于系统的错误,以及std::error_condition
,用于处理可移植的错误代码。下面我们将更详细地探讨这两种方式。
std::error_code
让我们重新设计之前的示例,提供一个用于创建具有特定目录路径的目录的函数:
#include <iostream>
#include <sys/stat.h>
std::error_code CreateDirectory(const std::string& dirPath) {
std::error_code ecode{};
if (mkdir(dirPath.c_str(), 0777) != 0) {
ecode = std::error_code{errno,
std::generic_category()}; // {1}
}
return ecode;
}
int main() {
auto ecode{CreateDirectory("/tmp/test")};
if (ecode){ // {2}
std::cerr << "Error 1: " << ecode.message() <<
'\n';
}
ecode = CreateDirectory("/tmp/test"); // {3}
if (ecode){
std::cerr << "Error 2: " << ecode.message() <<
'\n';
}
if (ecode.value() == EEXIST) {
std::cout << "This is platform specific and not
portable.\n";
}
return 0;
}
我们的新函数CreateDirectory
的客户端不再直接使用errno
变量来确定操作是否成功,而是使用标准库提供的一个实用类——