第19.4章:<stdexcept>
1. 人人都包含的那个头文件
<stdexcept> 是随遗留异常一起提供的那套类库,也是每个 C++ 代码库都会随手包含的那个头文件。它的全部内容就是 ::std::exception 下面这棵两分支的树:
std::exception what() -> char const *
├── std::logic_error "violated a precondition the caller could have checked"
│ ├── std::invalid_argument
│ ├── std::domain_error
│ ├── std::length_error
│ └── std::out_of_range
└── std::runtime_error "only detectable while running"
├── std::range_error
├── std::overflow_error
├── std::underflow_error
└── std::system_error (<system_error>, Ch19.5)
还有四个类住在别的头文件里,由运行时自己抛出:<new> 里的 ::std::bad_alloc 和 ::std::bad_array_new_length,<typeinfo> 里的 ::std::bad_cast 和 ::std::bad_typeid。它们中的每一个都存一段消息字符串,并通过 virtual what() 返回它。
2. 这个划分意味着什么,以及为什么它什么也没携带
标准的本意是:logic_error 用于调用者的 bug,也就是调用之前本可以检查的前提条件;runtime_error 用于只有运行中的程序才能发现的事情。这听起来像是信息,也是教程愿意花一页讲它的原因。
它什么也没携带。没有编译器检查它,没有捕获点能对它有反应,而这个区分在调用点是不可见的,因为函数的签名根本不提这个类型。catch (::std::exception const &) 里的调用者无法分辨自己是违反了前提条件还是磁盘满了,而事后这也不重要:恢复路径同样都是"放弃这次操作"。真正携带信息的是错误码,也就是赫布异常放进那两个字里的东西(第3.16章)。
| 类 | 标准的本意 | 程序拿它能做什么 |
|---|---|---|
invalid_argument |
某个参数是错的 | 什么也做不了;它在调用之后才到 |
out_of_range |
下标或键超出了定义域 | 什么也做不了;它描述的那个检查本该由你自己做 |
length_error |
容器装不下那么多元素 | 打印一条没人看的消息 |
overflow_error / underflow_error |
算术越界 | 什么也做不了 |
bad_alloc |
内存用完了 | 几乎永远没有正确的做法 —— 第19.2章 论证它应当终止进程 |
3. 怎么构造一个,以及它存了什么
构造函数有两个:(char const *) 和 (::std::string const &)。消息存在对象内部,这意味着带长消息的 runtime_error 构造函数会分配内存 —— 在失败路径上,在异常还没被抛出之前。
what() 返回 char const *,而在本书里那不能当作文本打印(第5.8章):你必须说明这个指针意味着什么,所以这里每个例子都写 ::fast_io::mnp::os_c_str(e.what())。C 字符串是一种约定,不是一个类型。
要给遗留异常加上你自己的东西,你必须从这些类之一派生,并把消息往上传给基类。没有别的办法:基类拥有唯一的出口通道。下面是一个带行号和列号的解析错误,被当作基类捕获,旁边是标准库自己的 length_error,来自一个容器:
#include <fast_io.h>
#include <stdexcept>
#include <string>
#include <vector>
/* the only way to add data to a legacy exception: derive and pass a message up */
struct parse_error : ::std::runtime_error
{
::std::size_t line;
::std::size_t column;
parse_error(::std::size_t l, ::std::size_t c)
: ::std::runtime_error("invalid token"), line(l), column(c)
{}
};
int main()
{
try
{
throw parse_error(12, 34);
}
catch (::std::exception const &e)
{
::fast_io::io::print("as exception ", ::fast_io::mnp::os_c_str(e.what()), "\n");
}
try
{
::std::vector<int> v;
v.resize(static_cast<::std::size_t>(1) << 63);
}
catch (::std::exception const &e)
{
::fast_io::io::print("as length ", ::fast_io::mnp::os_c_str(e.what()), "\n");
}
}
as exception invalid token
as length vector::_M_default_append
关于这段输出有两点。第一,parse_error 自己的数据 —— line 和 column —— 在捕获点已经没了:处理块是写给 ::std::exception 的,所以它能碰到的只有 what()。那些额外数据存在过、被付出过代价,而在不用 RTTI 和 dynamic_cast 的情况下取不回来(第18.5章)。
第二,标准库的消息是 vector::_M_default_append —— 一个私有成员函数的名字,而不是一句关于用户问题的话。这就是 what() 在实践中的样子:一段英文的实现细节字符串,没有程序能解析它,没有用户能对它做出反应。
4. 这个头文件招来的 bug:按值捕获
catch (T e) 对每个 T 都合法,而对多态的 T,它只拷贝异常对象的 T 那部分。对象剩下的部分连同 vtable 一起被切掉,所以你通过基类调用的 what() 就是基类的:
#include <fast_io.h>
#include <stdexcept>
int main()
{
try
{
throw ::std::out_of_range("index 7 is past the end");
}
catch (::std::exception e)
{
::fast_io::io::print("what ", ::fast_io::mnp::os_c_str(e.what()), "\n");
}
}
slice.cc: In function 'int main()':
slice.cc:10:33: warning: catching polymorphic type 'class std::exception' by value [-Wcatch-value=]
10 | catch (::std::exception e)
| ^
what std::exception
程序抛出的是 out_of_range("index 7 is past the end"),打印出来的却是 std::exception。消息不是丢了,而是被切掉了:处理块拥有的是 ::std::exception 子对象,它的 what() 就照实这么说。如果你要求警告(-Wcatch-value),编译器会告诉你,而每份遗留风格指南里都有一条关于"按 const& 捕获"的规定,原因正在于此。
5. 用什么替代它
赫布异常的世界里没有任何东西对应 <stdexcept>,也不需要。错误就是错误域里的一个错误码(第19.1章):错误码说明发生了什么,错误域说明这个码是什么意思,而 to_errc() 给每个调用者一门共同语言。没有消息字符串要分配,没有基类要派生,没有可被切片的 virtual what(),也没有会写错的处理块顺序。
当某个遗留库还是把这样的异常扔到你面前时,它会以一个带错误域和消息的 ::std::error 抵达(第19.6章),这正是互通层存在的全部理由。