[Translation] System error support in C++0x
- 本文翻译自 http://blog.think-async.com/
- 基于原文和自己的理解,如有错误欢迎指出。
1. part1
在 C++的众多新特性里,有一个小部分叫<system_error>
,它提供了一个管理系统错误的程序方法。
其中定义的组件主要有:
-
class error_category
-
class error_code
-
class error_condition
-
class system_error
-
enum class errc
我(原作者)曾参与过设计这一部分组件,所以在这个系列里我会尽力展现这个组件的原理、历史和预期的使用方式
1.1. 如何调用
Boost 库中包含了完整的实现,支持 C++03,在目前(原文撰写于 April 07, 2010)可能是经过测试的可移植性最强的实现。当然使用的时候需要带上命令空间 boost::system::
而不是 std::
在 GCC4.4以及更高的版本上,通过-std=c++0x 编译选项即可使用 std::system_error
另外,在 Microsoft Visual Studio 2010上会附带类的实现,其主要问题是 system_category()
无法正确的表示出 WIN32 errors,后续会说明
请注意这只是我知道的实现方法,可能还有别的方式。
1.2. 概述
以下是<system_error>
定义的类型
-
class error_category
作为基类,区分错误码(error_code
)或错误情况(error_condition
)的来源或者类别 -
class error_code
代表了一种特定的操作返回的错误值 -
class error_condition
你希望在代码中进行测试的一种情况 -
class system_error
当异常情况通过 throw / catch 抛出的时候,用来包装error_codes
的异常 -
enum class errc
一系列一般情形下的错误情况值,继承自 POSIX -
is_error_code_enum<>
,is_error_condition_enum<>
,make_error_code
,make_error_condition
把枚举类转换为error_code
/error_condition
的方法 -
generic_category()
返回一个类对象用来区分errc
-
errc
基本错误码(error codes
)和错误情况(error conditions
) -
system_category()
返回一个操作系统错误码的类对象
1.3. 原则
这一节列出了我在设计这部分模块的时候考虑到的一些原则,和大多数项目意义,其中一些是一开始就当作目标,还有一部分则是在开发过程中逐渐加上的。
1.3.1. 不是所有的错误都是异常
简单来说就是,抛异常并不总是解决错误的最好方法,在某些圈子里,这甚至是一个有争议的话题(虽然我并不懂为什么…) 比如说,在网络编程里,会经常遇见的错误:
- 你无法连接到目标远程 IP
- 你的连接中断了
- 你尝试使用 IPV6连接但是没有可用的 IPV6接口
当然这里面可能有异常情况,但是同时它们也可能是正常情况的一部分,如果你考虑周全,它们就不是异常:
- 这个 IP 地址是一个主机名的一系列 IP 地址之一,应该考虑尝试下一个
- 当前网络不可用,应该在尝试重新连接 N 次失败以后再放弃
- 程序在没有 IPV6接口时重新使用 IPV4接口
在 Asio 编程的情况下,另一个需求是将异步操作的结果传递给其完成处理程序的方法。这种情况,我希望操作的错误代码作为处理程序回调的参数。(另一种方法是提供在处理程序内重新抛出异常的方法,例如. net 的 BeginXYZ/EndXYZ 异步模式。在我看来,这种设计增加了复杂性,使 API 更容易出错。)
最后同样重要的一点时,由于代码大小和性能限制,一些情况下将无法或不愿意使用异常。
简而言之:要务实,不要教条。考虑好清晰度、正确性、约束条件,甚至个人品味后,使用任何最适合的错误机制。通常,在异常和错误代码之间做选择时是在使用的时候。这意味着项目里的系统错误工具应该同时支持这两种方法。
1.3.2. 多种来源的错误
C++03标准里把 errno 作为错误码的来源,也被用到了 stdio 函数、一些数学函数等里
在 POSIX 平台,许多系统操作都使用 errno 来传递错误,POSIX 定义了额外的 errno 错误码来覆盖这些情况。
另一方面在 Windows 下除了 C 标准库,没有使用 errno,Windows 的 API 调用通常通过 GetLastError 报告错误。
当考虑网络编程的时候,getaddrinfo
函数家族在 POSIX 上使用它自己的一组错误代码(EAI_...
),但是和 Windows 下的 GetLastError()
命令空间一样。整合了其他库(ssl,…)的程序会遇到其他类型的错误码。
程序应该能够统一的管理这些错误码,我特别关注的是如何通过组合来创建更高层次的抽象。把系统调用、getaddrinfo
、SSL 和普通库等等整合进一个统一的 API 来使得用户使用错误码不需要包含各种其他类型的错误码。给这个 API 添加新的错误码源也不应该改变接口。
[注:] 这部分描述历史原因和想要实现的目标,能力有限了解不深可能翻译不太准确。有能力建议读原文。
1.3.3. 要做到用户可拓展
使用标准库的使用者需要添加他们自己的错误源,这种能力可能只是被用来整合进一个第三方库,但是也实现了一个更高层次的抽象关联。当开发一个类似 HTTP 的协议实现的时候,我希望能够添加一系列定义在 RFC 里的错误码
1.3.4. 保留原始的错误码
这本来不是我的本意,我的想法是这个标准应该提供一系列总所周知的错误码,如果系统操作返回一个错误,库有责任把错误转化成一个大家熟悉的错误码(如果这样的映射有意义的话)
幸运的是有人指出了我的错误,转换也给错误码会丢失信息:是底层的系统调用的错误(而不是你写的代码出的错)。这可能在程序控制里不是什么大问题,但是却对程序可支持性影响挺大。毫无疑问程序员会使用标准错误码来记录跟踪问题,而丢失的原始错误信息可能在是诊断问题是至关重要的。
这个最后一个原则也很好地融入了第二部分的主题:error_code
和 error_condition
2. part2
在 C++一千多页的草案中,随性的读者肯定会注意到:error_code
和 error_condition
看起来十分相似!难道是个复制粘贴的错误么。
2.1. 你用来做什么才是最重要的
回顾一下我在 part1 给出的描述
-
class error_code
代表了一种特定的操作返回的错误值 -
class error_condition
你希望在代码中进行测试的一种情况
为了不同的使用目的,这两种类是有区别的。比如说,假设有一个函数 create_directory()
void create_directory(const std::string& pathname,std::error_code& ec);
然后这样来调用它:
这种调用时有可能因为各种原因而失败,诸如:
- 权限不足
- 这个目录已经存在
- 这个路径太长了
- 上级目录还不存在
不管是什么原因导致的失败,在 create_directory()
返回后,error_code
里会包含一个(可能因系统不同而不同的)错误码,如果成功调用则是0值,这符合了过去使用 errno
和 GetLastError()
的 0
作为成功时的返回值 && 非0
作为特定错误的传统。
如果只关心这个操作有没有成功,你当然也可以利用 error_code
可以直接隐式转换为 bool
类型的特性:
不过,假定你对想专门检查下是不是因为目录已经存在(directory already exists)。如果是这个原因,那我们的程序还可以继续跑下去,尝试写出了如下代码:
这样写代码是错的,你得摆脱原来用在 POSIX 平台上的做法,但是得记住 ec 还是有系统差异性(OS-specific)的,在 Windows 上,这个错误可能是叫做 ERROR_ALREADY_EXISTS
(或者更糟糕的情况代码不检查错误码的种类,我们后面会再提及这)。
2.2. 最重要的原则
不要这样调用:error_code::value()
我们现在根据这种有系统差异性的错误码(EEXIST
or ERROR_ALREADY_EXISTS
)来判断出这种出错的情况(“directory already exists”),自然而然的你就需要 error_condition
。
2.3. errorcode 和 errorconditions 的比较
当你想通过逻辑运算符!=
、==
来比较 error_code
和 error_condition
的时候有可能发生的情况:
-
error_code
和error_code
:检查精确匹配 -
error_condition
和error_condition
:检查精确匹配 -
error_code
和error_condition
:检查等价
我希望我表达的足够清楚,你需要把这种有系统差异性的错误码和代表“directory already exists”的错误情况对象进行比较。C++0x 刚好提供了一个标准:std::errc::file_exists
,所以代码应该是这样的:
能够这样使用是因为库的实现里在定义了错误码(error code)EEXIST
or ERROR_ALREADY_EXISTS
和错误情况的(error condition)std::errc::file_exists
的等价性。在后面的一期里,我会展示给你如何自定义自己的错误码&&错误情况以及让它们等价
(请注意,准确来说,std::errc::file_exists
是枚举类 errc 的一个枚举数,目前可以暂时把 std::errc::*
枚举器理解为 error_condition
常量的占位符,后续再进一步解释这是如何运作的。)
2.4. 如何确定你能测试哪些情况
在 C++0x 的一些新的库函数里有“Error conditions
”子句,这些子句列举了 error_condition
常量和这些常量在不同情况下对应的 error_code
2.5. 一点历史
最初的 error_code
类被建议用于 TR2,作为文件系统和网络库的辅助组件。在该设计中,实现了 error_code
常量,以便在可能的情况下匹配特定于操作系统的错误。如果不可能匹配,或者存在多个匹配,则在执行底层操作之后,库实现将从有系统差异性的错误转换为标准 error_code
。
在基于电子邮件的设计讨论中,我认识到了保留原始错误代码的价值。进一步,构造了 generic_error
类的原型。后将 generic_error
重命名为 error_condition
才是一个满意的解决方案。根据我的经验,命名是计算机科学中最困难的问题之一,一个好的名字会让你走更远。
接下来,来看看如何让 enum 类 errc
作为 error_condition
占位符使用。
3. part3
3.1. 枚举类作为类常量
前面提到的,<system_error>
里定义的 errc
枚举类:
这些枚举数等价于不同 error_condition
常量
很明显这之中有一个从 errc
到 error_condition
的单构造参数隐式转换,很简单吧。
3.2. 也不是这么简单
不是这么简单的原因:
-
枚举数提供了一个错误值,但是为了构建这个
error_condition
我们还需要知道它的种类。<system_error>
使用分类来支持不同的错误源,一个类别就包含了error_code
和error_condition
。 -
<system_error>
应该是用户可拓展的,也就是说用户(包括以后对标准库的拓展)都应该可以定义他们自己的 errc 占位符 -
<system_error>
应该同时支持给error_code
和error_condition
提供占位符,现在errc
的枚举类给error_condition
常量提供了占位符,有些情况下可能也需要给error_code
提供常量(来表示不同的 error_code) -
最后,
<system_error>
应该支持从枚举数到error_code
或error_condition
的显示转换,可移植程序或许需要创建从std::errc::*
枚举器继承来的错误码
所以这行代码
if (ec == std::errc::file_exists)
从 errc 隐式转换成了 erroc_condition,中间包含了几个步骤
3.3. Step 1 确定枚举数是错误码还是错误条件
有两种模板元来用来注册一个枚举类型:
如果一个种类是用 is_error_code_enum<>
来注册的就会被隐式转换成 errorcode,同理用 is_error_condition_enum<>
注册会被隐式转换为 errorcondition,而 enum class errc
是这样被注册的:
隐式转换是通过有条件地启用转换构造函数。这可能是使用SFINAE实现,不过我们只需要认为是:
所以我们写 if(ec==std::errc::file_exists)
,编译器会在这下面两个重载中选着:
他会选后者因为 error_condition
的转换构造函数可用。
3.4. Step 2 给一种错误类别关联一个值
一个 error_condition
对象包括两个属性:value
和 category
,现在我们需要让构造函数正确的初始化。
通过让构造调用 make_error_condition()
来实现,为了实现用户可拓展,这个方法通过 ADL(argument-dependent lookup)来定位。默认的,make_error_condition()
和 errc
一样是定义在 namespace std
里。
make_error_condition()
的实现很简单:
这个构造方法用两个参数构造 error_condition
来显式定义错误值和错误类别。
如果是在转换构造一个 error_code
,就用 make_error_code()
,在某些方面,error_code
和 error_condition
的构造其实是一样的。
3.5. Step 3 显示转换成 errorcode 或者 errorcondition
尽管 error_code
最开始是想用于有操作系统差异性的错误码,可移植性的代码需要用一个 errc
枚举元来构造一个 error_code
错误码。因此,make_error_code(errc)
和 make_error_condition(errc)
都被提供了,可移植性的代码可以这样使用它们:
3.6. 一点历史
最开始<system_error>
提案里把 error_code
常量定义为对象:
LWG(Library Working Group 库开发组)担心定义这么多全局对象需要的大小太过头了,要求提供另外一种可行的方法,我们研究过使用 constexpr
的可能性,最终发现和<system_error>
这个组件有一些地方不适用,这让使用 enum
成为了最好的选择。
后面,我会继续展示你该如何添加自己的 error codes
和 error conditions
。
3.7. 译注:需要补充的坑
- type_traits
- SFINAE
- truetype,falsetype
- ADL
这部分翻译有点吃力,理解并不透彻!
4. part4
4.1. 创建你自己的错误码
我在 part one 里就说过,设计<system_error>
的原则之一就是要支持用户自定义拓展,就是用户可以用这个工具来描述定义自己的错误码。
这一章里,我将概述一下你应该怎么做。举一个例子,假设你正在写一个 HTTP 库并且需要根据不同的 HTTP 返回的错误码处理的错误。
4.2. Step 1: 定义错误值
你首先需要定义一系列错误值,假设你在使用 C++0x,你可以用 enum class
,就像 std::errc
一样:
这些错误根据 HTTP 返回值来指定了不同的数值。很明显也很重要的是,当你使用这些错误码的时候,不要选0作为某个错误码的数值,你应该记得<system_error>
有转换:0 = success。
顺带提一句:如果为了兼容 C++03,你也可以去掉 class
关键字。
注:C++0x 的 enum class
和 enum
的区别就是前者把枚举元放进了类里,因此你必须要在前面加上类名才能访问它,比如 http_error::ok
,你可以近似的认为就像包进了命名空间一样:
后续我会使用 enum class
,读者可以自行尝试用命名空间包含。
4.3. Step 2: 定义一个 error_category
类
一个 error_code
对象包含着类别和值,类别决定了这个数值(比如100)是代表着 http_error::continue_request
、std::errc::network_down
(Linux 下是 ENETDOWN)或其他意思。
为了构造一个新类别,你必须继承 error_category
类:
目前我们只实现一下继承自 error_category
的虚函数。
4.4. Step 3: 给这个类别一个可读性强的名字
error_category::name()
这个虚函数必须返回一个代表类别的字符串:
这个名字不强制要求全局独一无二,因为它只有在把 error code 写入输出流时才被用到。尽管如此,对一个给定的程序而言,有一个独一无二的命名总是好的。
4.5. Step 4: 把 error codes 转换为字符串
error_category()::message()
方法把一个错误值转换成对应这个错误值的字符串:
当你调用 error_code::message()
时就可以把 error_code 转换成对应的错误信息了。
<system_error>
没有对这些消息的本土化(原文用的是 localisation,译者理解就是指不同系统环境下的不同)提供帮助,如果是库函数里的错误,会基于不同的环境给出不同的结果,如果你的程序也需要支持 localisation,我建议你用同样的方法。(一点历史:LWG 意识到过要支持 locallisation,但是由于无法和用户可拓展性协调好,最终选择了在标准中对这方面只字不提)
4.6. Step 5: 类别要唯一
一个继承自 error_category
的对象的唯一性是由其地址决定的,也就是当你这样写:
这里的 if
判断就等价于你这样写:
从这个标准库的例子可以看出,你需要提供一个方法来返回这个类别对象的引用:
这个方法必须始终返回同一个对象,一种实现方法是定义为全局对象:
然而用一个全局变量会出现在不同模组中使用初始化顺序的问题,另一个可选方案是用静态变量:
这样这个类的对象就会在第一次使用的时候被初始化好。C++0x 也保证了这个初始化是线程安全的(C++03没有保证)
一点历史:在早期设计阶段,我们考虑过使用整型或者字符串来标志一类 error_code,最主要的问题就是需要保证在和用户可拓展性结合的时候,还要保证独一无二的特性。如果一个类别是用整形或者字符串来定义的,那如何解决两个相关库的冲突?用类来作为标志符,可以用链接器来保证不同的类别会被不一样的识别。以及,用继承基类的方法,可以让我们保持错误码可复制的同时使用多态的特性。
4.7. Step 6: 从枚举里构建一个错误码
如同我在 part3 中所说,<system_error>
的实现要求 make_error_code()
方法来把一个错误码和类别联系起来。比如说还是 HTTP 错误,你可以像这样写:
更完整点,你还可以给错误情况也提供相似的方法:
因为<system_error>
实现的时候找这些方法都是通过 ADL,你需要把他们和 http_error
类放到同一个命名空间
4.8. Step 7: 注册一个隐式转换到 error_code
因为 http_error
枚举元被用作 error_code
常量,用 is_error_code_enum
模板元来实现一个转换构造:
4.9. Step 8: (可选)设置一个默认的错误情况
有些你定义的错误可能和标准库的 errc
错误情况意思相同。比如说,HTTP 应答码403 Forbidden
也基本上和 std::errc::permission_denied
相同
error_category::default_error_condition()
虚函数允许你对给定错误码定义等价(equivalent)的错误情况(见 part2 关于等价的定义)。
对于这个 HTTP 错误,你可以这样写:
如果你选者不重载这个虚函数,那么错误码默认的错误情况就是有相同错误值和类别的了(default)
4.10. 使用
你可以把 http_error
枚举元用作 error_code
常量了:
比如处理的同时改变错误码
以及来检验它:
有时候错误值基于 HTTP 应答码,我们可以直接用应答码来设置 error_code
:
最后如果你在 Step 8里定义了一个等价关系,那么你可以:
原始的错误码就保证了错误没有丢失,方便定位到错误发送的根源。
下一节,我会展示如何使用用户自定义的错误码。
5. part5
5.1. 创建你自己的 error condition
<system_error>
组件并不只是在 error_code
上支持用户可拓展,error_condition
也可以自定义。
5.2. 为什么需要自己的 error condition
为了回答这个问题,需要先回顾一下 error_code
和 error_condition
的区别:
-
class error_code
代表了一种特定的操作返回的错误值 -
class error_condition
你希望在代码中进行测试的一种情况
这是一些建议使用 error_condition 的情况
-
有操作系统差异性的错误的抽象
-
假设你正在写一个可移植性的方法
getaddrinfo()
,暂定两种错误情况:当前无法解析,请稍后再试
和无法解析
,而getaddrinfo()
返回错误又和平台相关-
在 POSIX 上,这俩错误码分别是
EAI_AGAIN
和EAI_NONAME
,且是在不同命名空间下的errno
值,意味着你需要实现一个新的error_category
错误类别来获取这些错误码 -
而在 Windows 上,这两个错误码分别是
WSAEAI_AGAIN
和WSAEAI_NONAME
,尽管名称上和 POSIX 的很像,但是共享Getlasterror
命名空间,因此你可能想复用std::system_category()
来代表getaddrinfo()
在 Windows 下的错误
-
在 POSIX 上,这俩错误码分别是
-
为了避免丢掉信息,你可能想在保留原始有平台差异的错误码的的同时提供两种错误情况
error_condition
(比方说叫做name_not_found_try_again
和name_not_found
),这样这个 API 的使用者就可以针对这种情况测试了
-
假设你正在写一个可移植性的方法
-
给通用的错误码一个和上下文相关的意思
-
大多数 POSIX 系统调用用
errno
来反馈错误,许多错误被复用在不同的功能里导致你需要查看相应的具体位置来判断到底是什么错误。如果你用这些系统调用来实现自己的代码,那么对用户来说这些错误就更摸不着头脑了。 -
比方说:你实现了一个简单的数据库,每一个条目(entry)都被存储在一个单独的文件里,当你试图读文件的时候,数据库调用
open()
方法来读取文件,这个方法设置了错误码ENOENT(if the file does not exist)
-
因为数据库的存储方法对于用户而言是抽象的,你不可能让用户知道这个意味着
no_such_file_or_directory
,事实上你可以创建你自己的富有语义的错误情况no_such_entry
等效表示ENOENT
-
大多数 POSIX 系统调用用
-
测试一系列相关的错误
-
随着你的代码库的增长,你也许发现有一些错误是类似的,也许你需要一个对系统可用资源低的反馈:
-
not_enough_memory
-
resource_unavailable_try_again
-
too_many_files_open
-
too_many_files_open_in_system
-
-
在不同的地方可能错误码不一样,但是对这些错误的反应方式都是一样的,所以如果有一个一致的表述:
low_system_resources
就可以方便的写如下代码来测试:
-
随着你的代码库的增长,你也许发现有一些错误是类似的,也许你需要一个对系统可用资源低的反馈:
5.3. Step 1 : 定义你自己的错误值
你需要创建一个 enum
枚举类给这些错误码,类似于 std::errc
:
这些值用多少其实不是很重要,只要保证他们各不相同且不为0,默认值0一般表示 success 没有错误。
5.4. Step 2 : 定义一个 error_category
类
一个 error_condition
对象包含错误值和种类,为了创建一个新类,你必须从 error_category
继承:
5.5. Step 3 : 给这个类别一个可读性强的名字
5.6. Step 4 : 把错误情况转换为字符串
error_category::message()
方法把错误值转换为一个表示这个错误的字符串(因此 enum 里的值并不重要):
当然你可能根本不打算调用这个方法,那么你可以简单的写写:
5.7. Step 5 : 实现错误的等价判断
虚函数 error_category::equivalent()
被用来定义 errorcode 和 errorcondition 的等价关系,有两种重载方法:
这种被用来建立当前种类下的 error_code
和任意 error_condition
的一致。
这种建立了当前种类下的 error_condition
和其他种类的 error_code
的等价关系。
因为你在创建自定义的 error_condition
,这个方法你必须重载。
定义等价关系很简单,如果你想要一个 error_code
等价你写的错误情况,就 return true
,否则 return false
如果你是想抽象一个有系统差异的错误,你就得这样实现:
你想写多复杂都行,甚至能复用其它 error_condition
.
如果你像创建一个语义相关的错误情况或者测试一些相关的错误:
5.8. Step 6 : 给种类一个独特的标志
你应该给构造的类一个引用:
const std::error_category & api_category();
为了总是使用同一个引用,你可以定位为全局变量:
或者用 C++0x 线程安全的静态变量
5.9. Step 7 : 从枚举里构造一个 error_condition
<system_error>
的实现要求一个 make_error_condition()
方法来把一个错误值关联到类里:
为了完整起见,同样还需要给 error_code
一个相似的构造函数,留给读者自己试试
5.10. Step 8 : 注册一个到 error_condition
的隐式转换
最终,为了 api_error
枚举器可以被用作 error_condition
的常量,需要一个 is_error_condition_enum
模板类的转换构造:
5.11. 使用 error_condition
现在 api_error
枚举器可以被用作 error_condition
常量了,就好像和 std::errc
里的一样使用:
就像我前面多次提及的,原始的错误码被保留没有丢失任何信息。不管错误码来自操作系统还是 HTTP 库还是自己的错误目录,你自定义的 error_conditions
都可以很好的匹配