第19.6章:跨越边界

English

1. 遗留异常到赫布异常:自动转换

你会调用那些靠抛出报告失败的库,而你改不了它们。这个方向不需要你写任何东西:一个遗留异常只要进入 throws 区域 —— catch throws 处理块里的调用,或来自标注了 throws 的函数的调用 —— 就会被自动转换成一个 ::std::error,不改源码、不加包装:


#include <fast_io.h>
#include <stdexcept>

/* a library we do not own: noexcept(false) is the legacy spelling of
   "this function may throw", and it is the only contract it can state */
void legacy_parser(int value) noexcept(false)
{
	if (value < 0)
	{
		throw ::std::runtime_error("negative input");
	}
}

/* our code: failures travel on the throws channel */
int parse(int value) throws
{
	legacy_parser(value);
	return value;
}

int main()
{
	try
	{
		::fast_io::io::print("parse(7)  = ", parse(7), "\n");
		int const bad{-1};
		::fast_io::io::print("parse(-1) = ", parse(bad), "\n");
	}
	catch throws (::std::error e)
	{
		::fast_io::io::print("caught ", e, "\n");
	}
}

parse(7)  = 7
caught [itanium_exception](St13runtime_error)negative input

parse 是一个 throws 函数,它从不提及异常。那个遗留库照样抛了,而它作为平台异常错误域里的两字值抵达调用者 —— 这里是 itanium_exception,也就是"这是一个装箱的 C++ 异常对象"。消息文本是运行时对被抛出对象的呈现:(St13runtime_error) 是修饰过的类型名,negative input 是 what()。

注意这个转换不是什么:它不是你必须写的 catch 子句,也不要求你付出一个遗留 try 块。展开仍然发生在遗留库内部;进入你代码的只是结果。这正是这套设计的要点 —— 边界只被跨它的人付一次代价,对上面所有人都不可见。

2. 重新抛出赫布异常

在处理块内部,裸写的 throw throws; 会重新抛出同一个错误,不拷贝、不添加任何信息,和遗留的 throw; 完全一样。当你需要补充上下文,或者判断某些错误码不该由你吞掉时,就用它:


#include <fast_io.h>

int read_value(int value) throws
{
	try
	{
		if (value < 0)
		{
			throw throws ::std::errc::invalid_argument;
		}
		return value;
	}
	catch throws (::std::error e)
	{
		if (e.equivalent(::std::errc::invalid_argument))
		{
			throw throws;
		}
		return -1;
	}
}

int main()
{
	try
	{
		::fast_io::io::print("read_value(5)  = ", read_value(5), "\n");
		::fast_io::io::print("read_value(-1) = ", read_value(-1), "\n");
	}
	catch throws (::std::error e)
	{
		::fast_io::io::print("caught ", e, "\n");
	}
}


read_value(5)  = 5
caught [posix](22)Invalid argument

这份输出值得细读。第一行打印了,第二行抛出了,而 read_value(-1) = 这段文本从未出现 —— 因为 print 在写出任何东西之前先求值它的实参,所以在格式化途中失败就意味着没有任何部分输出。这是单次 print 调用的性质,也是"一行一个调用、不要用多次写入拼一行"的又一个理由。

3. 赫布异常到遗留异常:必须你主动要求

反方向不自动,而且绝不该自动。把一个赫布异常转换成 C++ 异常,意味着付出 第19.8章 测量过的 unwinder、堆分配和 RTTI 的代价 —— 这个代价如果悄悄出现在从未要求它的代码里,那就糟了。所以语言要求你自己写出来:


#include <fast_io.h>
#include <stdexcept>

int main()
{
	try
	{
		try
		{
			throw throws ::std::errc::value_too_large;
		}
		catch throws (::std::error e)
		{
			/* a handler cannot call a throwing function: no second channel */
			e.throw_legacy_exception();
		}
	}
	catch (::std::exception const &ex)
	{
		::fast_io::io::print("legacy side ", ::fast_io::mnp::os_c_str(ex.what()), "\n");
	}
}


legacy side [posix](75)Value too large for defined data type

e.throw_legacy_exception() 是 [[noreturn]] 的:它要么把这个错误作为平台原生异常类型重新抛出(当错误域实现了 do_throw_legacy_exception 时),要么把它装进 ::std::cxx_std_error_legacy_exception —— 后者派生自 ::std::exception,并在构造时缓存消息。你捕获到的就是这个盒子:what() 返回了 [posix](75)Value too large for defined data type —— 错误域名、错误码和消息,全都由那个在别处打印错误时用的同一个 do_query_information 钩子产生。

路上会丢掉两样东西,两样你都该知道。拥有内存的错误码(也就是 do_cleanup 非空的错误域)不能靠拷贝装箱 —— 两份拷贝会释放同一个东西 —— 所以它会先通过 do_to_errc 被降级到什么都不拥有的 POSIX 错误域。而错误域的全部意义 —— 一个带名字的错误码空间 —— 会坍缩成一段消息字符串和一个 what()。遗留机制只能携带这些,所以活下来的也就只有这些。

还有一个更快的兄弟函数 throw_legacy_exception_no_what(),它跳过消息查询,因此抛出时不做任何分配。当遗留代码只是捕获来清理、从不读那段文本时,就用它。

还有一条关于你所处处理块的规则:让它保持简单。catch throws (::std::error e) 处理块没有第二条通道,所以它只该做转换然后离开,不要做会失败的工作。