处理异常和错误是现代编程中的重要一环,许多框架中都有一些API会返回错误以供处理. 在现代c++中,也有专门用于处理的方法
old-style
程序错误通常分为两类:
- 编程错误导致的逻辑错误。 例如,“索引超出范围”错误。
- 超出程序员控制的运行时错误。 例如,“网络服务不可用”错误。
在 C 样式的编程和 COM 中,错误报告的管理方式是返回一个表示错误代码或特定函数的状态代码的值,或者设置一个全局变量,调用方可以在每次执行函数调用后选择性地检索该变量来查看是否报告了错误。
在c语言中使用errno来表示错误,当出现错误时,errno会被修改为对应错误代码,通过strerror转为对应错误信息.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main() {
errno = ENODATA;
setlocale(LC_ALL, "en_US.utf8");
std::puts("Hello, World!");
FILE *fp;
fp = fopen("file.txt", "r");
if (fp == NULL) {
fprintf(stderr, "Value of errno: %d\n", errno);
fprintf(stderr, "Error opening file: %s\n", strerror(errno));
perror("Error printed by perror");
} else {
fclose(fp);
}
return 0;
}
常见的 errno
值
EPERM
:操作不允许ENOENT
:没有这样的文件或目录ESRCH
:没有这样的进程EINTR
:中断的系统调用EIO
:输入/输出错误
c++中的std::errc
如果要自己创建业务上的错误码,可以考虑enum
或者enum class
,有更好的语义.c++标准库中有std::errc
这个枚举类,起到错误码的作用1
2
3
4
5
6
7
8
9
10
11_STD_BEGIN
_EXPORT_STD enum class errc { // names for generic error codes
address_family_not_supported = 102, // EAFNOSUPPORT
address_in_use = 100, // EADDRINUSE
address_not_available = 101, // EADDRNOTAVAIL
already_connected = 113, // EISCONN
argument_list_too_long = 7, // E2BIG
argument_out_of_domain = 33, // EDOM
bad_address = 14, // EFAULT
...
_STD_END
c++中的std::error_code
此外标准库中还有std::error_code
和std::error_category
,这样相当于提供了分类,error_category有不同名字用以区分,继承error_category,实现name和message方法.
1
2
3auto error_code = std::make_error_code(std::errc::invalid_argument);
std::cout<<error_code.message()<<std::endl;
std::cout<<error_code.value()<<std::endl;
新式 C++ 中优先使用异常的原因如下:
- 异常会强制调用代码识别并处理错误状态。 未经处理的异常会停止程序执行。
- 异常跳转到调用堆栈中可以处理错误的位置。 中间函数可以让异常传播。 这些函数不必与其他层协调。
- 引发异常后,异常堆栈展开机制将根据妥善定义的规则销毁范围内的所有对象。
- 异常可以在检测错误的代码与处理错误的代码之间实现明确的分离
在c++中目前常用try
,catch
处理异常1
2
3
4
5try {
func(3);
} catch (const std::invalid_argument& e) {
std::cerr << e.what() << '\n';
}
异常来通常来自std::exception
或标准库中定义的派生类,也可以自己派生std::exception
异常类.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using namespace std;
struct MyException : public exception
{
const char * what () const throw ()
{
return "C++ Exception";
}
};
int main()
{
try
{
throw MyException();
}
catch(MyException& e)
{
std::cout << "MyException caught" << std::endl;
std::cout << e.what() << std::endl;
}
catch(std::exception& e)
{
//其他的错误
}
}
如果无法找到当前异常的匹配处理程序(或省略号 catch
处理程序),则调用预定义的 terminate
运行时函数.erminate
的默认操作是调用 abort
。 如果你希望 terminate
在退出应用程序之前调用程序中的某些其他函数,则用被调用函数的名称作为其单个自变量调用 set_terminate
函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// #include <cerrno>
void func(int c) {
if (c > std::numeric_limits<char>::max()) {
throw std::invalid_argument("Invalid argument");
}
}
void term_func() {
std::cerr << "terminate handler called\n";
std::abort();
}
int main() {
std::set_terminate(term_func);
setlocale(LC_ALL, "en_US.utf8");
std::puts("Hello, World!");
FILE* fp;
fp = fopen("file.txt", "r");
if (fp == NULL) {
fprintf(stderr, "Value of errno: %d\n", errno);
fprintf(stderr, "Error opening file: %s\n", strerror(errno));
perror("Error printed by perror");
} else {
fclose(fp);
}
try {
func(3);
} catch (const std::invalid_argument& e) {
std::cerr << e.what() << '\n';
}
return 0;
}
std::optional
类模板 std::optional
管理一个可选的所含值,即既可以存在也可以不存在的值。
一种常见的 对应的类型/类型模板 类模板 在c++23中实现,类模板 1) 主模板。在自身的存储中包含预期值或非预期值。不会进行动态分配。 如果程序以引用类型、函数类型,或 std::unexpected的特化实例化 非预期值是类模板 用非对象类型、数组类型、 欢迎关注我的其它发布渠道optional
使用情况是作为可能失败的函数的返回值。与如 std::pairoptional
可以很好地处理构造开销高昂的对象,并更加可读,因为它明确表达了意图。optional<T>
的任何实例在任意给定时间点要么含值,要么不含值。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// optional 可用作可能失败的工厂的返回类型
std::optional<std::string> create(bool b)
{
if (b)
return "Godzilla";
return {};
}
// 能用 std::nullopt 创建任何(空的)std::optional
auto create2(bool b)
{
return b ? std::optional<std::string>{"Godzilla"} : std::nullopt;
}
int main()
{
std::cout << "create(false) 返回 "
<< create(false).value_or("empty") << '\n';
// 返回 optional 的工厂函数可用作 while 和 if 的条件
if (auto str = create2(true))
std::cout << "create2(true) 返回 " << *str << '\n';
}1
2auto oDouble = std::make_optional(3.0);
auto oComplex = make_optional<complex<double>>(3.0, 4.0);std::in_place
、 std::in_place_type
和 std::in_place_index
是消歧义标签,能传递给std::optional 、std::variant和std::any的构造函数,以指示应该原位构造对象,以及(对于后二者)要构造的对象的类型。std::in_place_t
、 std::in_place_type_t
和 std::in_place_index_t
能用于构造函数的参数列表中,以匹配有意的标签std::variant
std::variant
表示一个类型安全的联合体(以下称“变体”).一个 std::variant
的实例在任意时刻要么保有它的可选类型之一的值,要么在错误情况下无值
在 invariant std::variant1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int main()
{
std::variant<int, float> v, w;
std::variant<int,double> myv{std::in_place_index<0>,1,std::in_place_index<1>,1.2f};
v = 42; // v 含 int
int i = std::get<int>(v);
assert(42 == i); // 成功
w = std::get<int>(v);
w = std::get<0>(v); // 与前一行效果相同
w = v; // 与前一行效果相同
// std::get<double>(v); // 错误:[int, float] 中无 double
// std::get<3>(v); // 错误:有效索引值为 0 与 1
try
{
std::get<float>(w); // w 含 int 而非 float:会抛出异常
}
catch (const std::bad_variant_access& ex)
{
std::cout << ex.what() << '\n';
}
using namespace std::literals;
std::variant<std::string> x("abc");
// 转换构造函数在无歧义时起作用
x = "def"; // 转换赋值在无歧义时亦起作用
std::variant<std::string, void const*> y("abc");
// 传递 char const* 时转换成 void const*
assert(std::holds_alternative<void const*>(y)); // 成功
y = "xyz"s;
assert(std::holds_alternative<std::string>(y)); // 成功
}std::expected
std::expected
提供表示两个值之一的方式:它要么表示一个 T
类型的预期 值,要么表示一个 E
类型的非预期 值。std::expected
决不会无值。
2) void 部分特化。表示一个 void 类型的预期值或在自身的存储中包含非预期值。不会进行动态分配。expected
,那么程序非良构.另外,T
必须不是std::in_place_t或std::unexpect_tstd::unexpected
代表一个 std::expected 中存储的非预期值。特别地,std::expected 具有接受 std::unexpected
为唯一实参的构造函数,创建含有非预期值的expected 对象。std::unexpected
的特化或有 cv 限定的类型实例化 unexpected
的程序非良构1
2
3
4
5
6
7
8
9auto parse_num(std::string_view& str)->std::expected<int, std::string> {
if (str.empty()) {
return std::unexpected<std::string>("empty string");
}
size_t pos;
int i = std::stoi(std::string(str), &pos);
str.remove_prefix(pos);
return i;
}参考资料