现代C++中的异常处理

处理异常和错误是现代编程中的重要一环,许多框架中都有一些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
#include <cerrno>
#include <clocale>
#include <cstdio>
#include <cstdlib>
#include <cstring>

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_codestd::error_category,这样相当于提供了分类,error_category有不同名字用以区分,继承error_category,实现name和message方法.

error_code

1
2
3
auto 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
5
try {
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
#include <iostream>
#include <exception>
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>
#include <cerrno>
#include <clocale>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <limits>
#include <stdexcept>
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 管理一个可选的所含值,即既可以存在也可以不存在的值。

一种常见的 optional 使用情况是作为可能失败的函数的返回值。与如 std::pair 等其他手段相比,optional 可以很好地处理构造开销高昂的对象,并更加可读,因为它明确表达了意图。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
#include <iostream>
#include <optional>
#include <string>

// 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
2
auto oDouble = std::make_optional(3.0);
auto oComplex = make_optional<complex<double>>(3.0, 4.0);

std::in_placestd::in_place_typestd::in_place_index 是消歧义标签,能传递给std::optional 、std::variant和std::any的构造函数,以指示应该原位构造对象,以及(对于后二者)要构造的对象的类型。

对应的类型/类型模板 std::in_place_tstd::in_place_type_tstd::in_place_index_t 能用于构造函数的参数列表中,以匹配有意的标签

std::variant

类模板 std::variant 表示一个类型安全的联合体(以下称“变体”).一个 std::variant 的实例在任意时刻要么保有它的可选类型之一的值,要么在错误情况下无值

  • std::in_place_type - 用于指定你想在 variant 里改变或者设定哪个类型
  • std::in_place_index - 用于指定你想改变或者设定的索引。类型从0开始枚举。
    在 invariant std::variant 中 - int 的索引是0,float的索引是1,string的索引是2。索引和 variant::index 方法的返回值是一样的
    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
    39
    40
    #include <cassert>
    #include <iostream>
    #include <string>
    #include <variant>

    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

    在c++23中实现,类模板 std::expected 提供表示两个值之一的方式:它要么表示一个 T 类型的预期 值,要么表示一个 E 类型的非预期 值。std::expected 决不会无值。

    1) 主模板。在自身的存储中包含预期值或非预期值。不会进行动态分配。
    2) void 部分特化。表示一个 void 类型的预期值或在自身的存储中包含非预期值。不会进行动态分配。

    如果程序以引用类型、函数类型,或 std::unexpected的特化实例化 expected,那么程序非良构.另外,T 必须不是std::in_place_t或std::unexpect_t

    非预期值是类模板 std::unexpected 代表一个 std::expected 中存储的非预期值。特别地,std::expected 具有接受 std::unexpected 为唯一实参的构造函数,创建含有非预期值的expected 对象。

    用非对象类型、数组类型、std::unexpected 的特化或有 cv 限定的类型实例化 unexpected 的程序非良构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    auto 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;
    }

    参考资料

    1. C++ 异常处理 | 菜鸟教程 (runoob.com)
    2. Customizing Error Codes (breese.github.io)
    -------------本文结束感谢您的阅读-------------
    感谢阅读.

    欢迎关注我的其它发布渠道