概述
根据错误的等级可以分为两类,一类是可恢复错误,一类是不可恢复错误,后者来说处理起来比较简单,因为发生错误且不可逆转,这时候往往只需要把程序关闭即可,所以后面讨论的默认是前者。
返回错误码
最简单和传统的方法:
优点:
- 简单直观:返回错误码是一种简单且广泛理解的方法,易于实现和使用。
- 控制流明确:调用者可以通过检查返回值来决定如何进行错误处理,控制流清晰。
- 性能:返回错误码通常具有很高的性能,因为它只需要返回一个整数,没有额外的异常处理开销。
- 兼容性:由于C语言中广泛使用错误码,许多现有的库和系统调用都返回错误码,这使得它具有良好的兼容性。
- 丰富的错误信息:可以通过定义不同的错误码来传达多种错误情况,提供丰富的错误信息。
- 避免异常的开销:与异常处理相比,返回错误码避免了异常抛出和捕获的运行时开销。
- 适用于资源受限的环境:在内存或处理能力受限的系统中,返回错误码是一种节省资源的错误处理方式。
缺点:
- 错误处理代码分散:错误检查和处理通常分散在调用栈的多个位置,可能导致代码难以阅读和维护。
- 容易遗漏错误处理:开发者可能会忘记检查错误码,导致未处理的错误被忽略。
- 错误码的全局性:错误码通常是全局定义的,可能导致不同模块之间的错误码冲突。
- 减少函数的返回能力:函数如果使用整数作为错误码,就无法同时返回有意义的结果和错误指示,限制了函数的返回类型。
- 错误处理不一致:不同的开发者或团队可能会使用不同的错误码约定,导致项目中的错误处理不一致。(遇到过)
- 降低了代码的可读性:错误码需要开发者去查阅文档以了解每种错误码的含义,降低了代码的直观性。
- 异常安全性:与RAII(资源获取即初始化)和异常处理相比,返回错误码不保证异常安全,因为资源可能在发生错误时没有被适当释放。
- 不利于函数的重构:如果函数的返回类型是错误码,当需要修改函数以返回额外信息时,可能会更加困难。
- 调试困难:当错误码未被适当处理时,程序可能会继续执行,导致难以追踪的错误和状态。
- 污染接口或者返回值:错误码有时候会跟随函数签名,不直观。
std::optional
std::optional
是一个在C++17中引入的模板类,它可以用来包装一个可能存在或不存在的值。虽然std::optional
主要用于表示可选值,但它也可以用于错误处理,使用std::nullopt
来表示返回值出错,优点和缺点都很明显。
优点:
- 表达可能的“无值”状态:
std::optional
可以自然地表达一个函数可能不返回有效结果的情况,这与传统的错误码返回(总是有值)形成对比。- 避免魔术值:使用
std::optional
可以避免使用特定的返回值作为错误指示(例如,-1或nullptr),这样可以减少对“魔术值”的依赖。- 类型安全:
std::optional
提供了类型安全的错误处理方式,编译器可以帮助确保正确处理了可选值的存在与否。- 现代C++特性:作为现代C++的一部分,
std::optional
与语言的其他特性(如自动类型推断auto
)协同工作良好。- 简洁性:使用
std::optional
可以使代码更加简洁,特别是在简单的错误处理场景中。- 与标准库的一致性:使用
std::optional
可以使代码与C++标准库的其他部分保持一致性。- 使用方便: 直接使用
opt.value()
,内部会帮助判断是否为std::nullopt
,是则抛出异常。缺点:
- 语义不明确:
std::optional
主要用于表达“值可能不存在”,而不是“发生了错误”,这可能导致语义上的混淆。- 丢失错误信息:与传统的错误码相比,
std::optional
不直接提供错误的详细信息,可能需要额外的机制来传递错误信息(非常致命)。- 性能开销:与返回简单状态码相比,
std::optional
可能会有轻微的性能开销,因为它需要额外的存储空间和构造函数调用。- 使用复杂性:在复杂的错误处理场景中,
std::optional
可能不如专门的错误处理机制(如std::expected
或异常)直观和方便。- 需要额外的错误处理逻辑:使用
std::optional
可能需要编写额外的代码来检查值是否存在,并相应地处理错误情况。- 不是所有错误都适用:对于一些需要返回详细错误信息或状态的错误处理场景,
std::optional
可能不是最佳选择。
多态错误码思想
利用enum
对状态码进行定义,注意使用强类型避免能从int转换,至于如何转换先前的博文已经有介绍过了,Cpp 中如何优雅进行 enum 到 string 的转换。
enum class LogicErrc: std::uint8_t{
success=0,
not_valid,
not_login
}
这时候注意到小标题,这跟多态错误码有什么关系?因为有些时候函数除了自定义的错误意外,还会出现诸如文件符操作失败,网络连接失败等原因,这时候我们需要兼容这类情况,该怎么做?
包装一个自己的类,加上对应的成员函数,相信应该知道怎么操作了,当然缺点也很明显,需要设计和实现一套完整的错误码和异常类体系,还需要处理传入参数的不同。
std::error_code
std::error_code
是 C++ 标准库中定义的一个类,它在 <system_error>
头文件中,用于表示错误的状态码。这个类是 C++11 引入的,目的是提供一种标准的方式来报告和处理错误,它与 POSIX 风格的 errno
系统相兼容,并且可以扩展以支持其他错误报告机制。以下是 std::error_code
的一些关键特性:
特性
- 兼容性:
std::error_code
可以存储来自操作系统的错误码,并且与errno
兼容。 - 错误分类:它与
std::error_category
一起使用,后者定义了错误码的分类,例如系统错误、通用错误等。 - 消息获取:可以通过
std::error_code
获取错误码对应的消息描述。 - 比较操作:
std::error_code
支持比较操作,允许你检查两个错误码是否相等或不等。 - 清空:可以清空
std::error_code
对象,使其表示“没有错误”。 - 转换:可以将
std::error_code
转换为int
类型,以便于与 POSIX 风格的 API 交互。
与多态错误码结合
class ErrorCategory : public std::error_category{
virtual string message(int ev)=0;
}
class StdErrorCategory : public ErrorCategory{
public:
const char* name() const noexcept{
return "stdError";
}
std::string message(int ev) const override {
// 根据错误码ev返回错误消息
return std::stderror(ev);
}
};
class MyErrorCategory : public ErrorCategory {
public:
const char* name() const noexcept {
return "MyError";
}
std::string message(int ev) const override {
// 根据错误码ev返回错误消息
return "Error description";
}
};
const MyErrorCategory myErrorCategory;
std::error_code error(1, myErrorCategory);//注意这里
完全可以把message设为虚函数,这时候就能比较方便的实现与标准库结合的错误码输出处理。
优点:
- 类型安全:使用
std::error_code
可以避免使用魔术数字作为错误码。 - 表达性:它提供了一种更表达性的方式来处理错误。
- 扩展性:可以定义新的
error_category
来扩展错误码的种类。
缺点:
- 复杂性:对于简单的错误处理,使用
std::error_code
可能显得有些复杂。 - 性能:与简单的整数错误码相比,对象可能涉及额外的性能开销。
- 使用习惯:需要开发者适应这种新的错误处理方式。
std::variant与std::expected
std::variant
和 std::expected
都是 C++ 中处理值多样性的工具,但它们在错误处理方面的使用各有特点和适用场景。
使用 std::variant
处理错误
std::variant
可以用来表示一个值,这个值可以是多种类型中的一个。在错误处理的上下文中,std::variant
可以持有一个成功值或者一个错误值。
优点:
- 可以存储不同类型的值,包括错误信息。
- 利用
std::holds_alternative
可以检查当前存储的类型。
缺点:
- 需要手动管理错误信息的类型。
- 错误处理逻辑可能分散在代码各处。
示例:
std::variant<int, std::string> divide(int a, int b) {
if (b == 0) {
return "Error: Division by zero";
}
return a / b;
}
void handle_division(int a, int b) {
auto result = divide(a, b);
if (std::holds_alternative<std::string>(result)) {
std::cout << std::get<std::string>(result) << std::endl;
} else {
std::cout << "Result: " << std::get<int>(result) << std::endl;
}
}
使用 std::expected
处理错误
std::expected<T, E>
是为错误处理设计的类型,其中 T
是期望的成功值类型,而 E
是错误值的类型。
优点:
- 明确区分了成功值和错误值,语义清晰。
- 提供了
has_value
和has_error
成员函数来检查状态。 - 错误处理逻辑集中,易于理解和使用。
缺点:
- 需要包含错误类型的定义,可能增加复杂性。
- 目前
std::expected
还未成为正式的 C++ 标准。
示例(假设 std::expected
已可用):
std::expected<int, std::error_code> divide(int a, int b) {
if (b == 0) {
return std::unexpected(std::make_error_code(std::errc::invalid_argument));
}
return a / b;
}
void handle_division(int a, int b) {
auto result = divide(a, b);
if (result.has_value()) {
std::cout << "Result: " << result.value() << std::endl;
} else {
std::cout << "Error: " << result.error().message() << std::endl;
}
}
总结
std::variant
提供了一种通用的方法来处理多种类型的值,包括成功值和错误值,但它需要手动检查和处理错误。std::expected
是专门为表示操作结果和错误设计的,提供了更明确和方便的错误处理机制,但可能需要额外的错误类型定义。
在实际应用中,如果错误处理是主要关注点,std::expected
是更合适的选择。如果需要处理多种类型的值,并且错误只是其中一种可能性,std::variant
可能更灵活。
throw std::system_error
在应用程序中使用系统错误时通常会与 std::error_code
一起使用,std::error_code
封装了来自操作系统的错误代码。使用 std::system_error
的典型场景是将从系统调用返回的错误代码转换为异常,然后抛出。
以下是如何抛出 std::system_error
的示例:
#include <iostream>
#include <system_error>
#include <cstdio> // 用于示例的文件操作
void some_system_call() {
// 假设我们调用一个系统函数,如 fopen,它可能会失败
FILE* file = std::fopen("somefile.txt", "r");
if (!file) {
// 获取 errno 来获取错误代码
int err_code = errno;
// 抛出 std::system_error,传递错误代码和相关信息
throw std::system_error(err_code, std::system_category(), "Failed to open file");
}
// 如果 fopen 成功,不要忘记在适当的时候 fclose(file);
}
int main() {
try {
some_system_call();
} catch (const std::system_error& e) {
std::cerr << "System error: " << e.what() << std::endl;
// 处理错误,例如清理资源、退出程序等
}
return 0;
}
如果 fopen
调用失败,函数 some_system_call
会抛出一个 std::system_error
异常。异常构造函数接收三个参数:
- 错误代码:
errno
表示的错误代码。 - 错误类别:
std::system_category()
提供了系统错误的类别。 - 描述信息:一个描述错误的字符串。
在 main
函数中,我们使用 try-catch
块来捕获并处理 std::system_error
异常。
抛出 std::system_error
的优点:
- 明确地报告了系统级错误,使得错误处理更加清晰。
- 异常信息中包含了错误代码和描述,便于调试和日志记录。
- 与标准库中的异常处理机制兼容。
抛出 std::system_error
的缺点:
- 抛出异常可能会带来性能开销,尤其是在异常频繁发生的情况下。
- 需要确保异常被适当地捕获和处理,否则可能导致程序异常终止。
- 在一些需要严格错误处理策略的系统中,使用异常可能不被推荐。