第19.4章:<stdexcept>

English

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章),这正是互通层存在的全部理由。