[Translation] System error support in C++0x

  1. 本文翻译自 http://blog.think-async.com/
  2. 基于原文和自己的理解,如有错误欢迎指出。

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. 不是所有的错误都是异常

简单来说就是,抛异常并不总是解决错误的最好方法,在某些圈子里,这甚至是一个有争议的话题(虽然我并不懂为什么 比如说,在网络编程里,会经常遇见的错误:

  1. 你无法连接到目标远程 IP
  2. 你的连接中断了
  3. 你尝试使用 IPV6连接但是没有可用的 IPV6接口

当然这里面可能有异常情况,但是同时它们也可能是正常情况的一部分,如果你考虑周全,它们就不是异常:

  1. 这个 IP 地址是一个主机名的一系列 IP 地址之一,应该考虑尝试下一个
  2. 当前网络不可用,应该在尝试重新连接 N 次失败以后再放弃
  3. 程序在没有 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_codeerror_condition

2. part2

在 C++一千多页的草案中,随性的读者肯定会注意到:error_codeerror_condition 看起来十分相似!难道是个复制粘贴的错误么。

2.1. 你用来做什么才是最重要的

回顾一下我在 part1 给出的描述

  • class error_code 代表了一种特定的操作返回的错误值
  • class error_condition 你希望在代码中进行测试的一种情况

为了不同的使用目的,这两种类是有区别的。比如说,假设有一个函数 create_directory()

void create_directory(const std::string& pathname,std::error_code& ec);

然后这样来调用它:

std::error_code ec;

create_directory("/some/path", ec);

这种调用时有可能因为各种原因而失败,诸如:

  1. 权限不足
  2. 这个目录已经存在
  3. 这个路径太长了
  4. 上级目录还不存在

不管是什么原因导致的失败,在 create_directory()返回后,error_code 里会包含一个(可能因系统不同而不同的)错误码,如果成功调用则是0值,这符合了过去使用 errnoGetLastError()0 作为成功时的返回值 && 非0 作为特定错误的传统。

如果只关心这个操作有没有成功,你当然也可以利用 error_code 可以直接隐式转换为 bool 类型的特性:

std::error_code ec;

create_directory("/some/path", ec);

if(!ec){

  // Success.

} else {

  // Failure.

}

不过,假定你对想专门检查下是不是因为目录已经存在(directory already exists)。如果是这个原因,那我们的程序还可以继续跑下去,尝试写出了如下代码:

std::error_code ec;

create_directory("/some/path", ec);

if(ec.value() == EEXIST) //NO!

  ...

这样写代码是错的,你得摆脱原来用在 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_codeerror_condition 的时候有可能发生的情况:

  • error_codeerror_code :检查精确匹配
  • error_conditionerror_condition :检查精确匹配
  • error_codeerror_condition :检查等价

我希望我表达的足够清楚,你需要把这种有系统差异性的错误码和代表directory already exists的错误情况对象进行比较。C++0x 刚好提供了一个标准:std::errc::file_exists,所以代码应该是这样的:

std::error ec;

create_directory("/some/path", ec);

if (ec == 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 枚举类:

enum class errc{

  address_family_not_supported,

  address_in_use,

  ...

  value_too_large,

  wrong_protocol_type,

};

这些枚举数等价于不同 error_condition 常量

std::error_code ec;

create_directory("/some/path", ec);

if (ec == std::errc::file_exists)

  ...

很明显这之中有一个从 errcerror_condition 的单构造参数隐式转换,很简单吧。

3.2. 也不是这么简单

不是这么简单的原因:

  • 枚举数提供了一个错误值,但是为了构建这个 error_condition 我们还需要知道它的种类。<system_error>使用分类来支持不同的错误源,一个类别就包含了 error_codeerror_condition
  • <system_error>应该是用户可拓展的,也就是说用户(包括以后对标准库的拓展)都应该可以定义他们自己的 errc 占位符
  • <system_error>应该同时支持给 error_codeerror_condition 提供占位符,现在 errc 的枚举类给 error_condition 常量提供了占位符,有些情况下可能也需要给 error_code 提供常量(来表示不同的 error_code)
  • 最后,<system_error>应该支持从枚举数到 error_codeerror_condition 的显示转换,可移植程序或许需要创建从 std::errc::*枚举器继承来的错误码

所以这行代码

if (ec == std::errc::file_exists)

从 errc 隐式转换成了 erroc_condition,中间包含了几个步骤

3.3. Step 1 确定枚举数是错误码还是错误条件

有两种模板元来用来注册一个枚举类型:

template <class T>

struct is_error_code_enum

  : public false_type {};



template <class T>

struct is_error_condition_enum

  : public false_type {};

如果一个种类是用 is_error_code_enum<>来注册的就会被隐式转换成 errorcode,同理用 is_error_condition_enum<>注册会被隐式转换为 errorcondition,而 enum class errc 是这样被注册的:

template<>

struct is_error_condition_enum<errc>

  : true_type{};

隐式转换是通过有条件地启用转换构造函数。这可能是使用SFINAE实现,不过我们只需要认为是:

class error_condition

{

  ...

  // Only available if registered

  // using is_error_condition_enum<>

  template <class ErrorConditionEnum>

  error_condition(ErrorConditionEnum e);

  ...

};

class error_code

{

  ...

  // Only available if registered

  // using is_error_code_enum<>.

  template <class ErrorCodeEnum>

  error_code(ErrorCodeEnum e);

  ...

};

所以我们写 if(ec==std::errc::file_exists),编译器会在这下面两个重载中选着:

bool operator==(

  const error_code &a,

  const error_code &b);

bool operator==(

  const error_code &a,

  const error_condition &b);

他会选后者因为 error_condition 的转换构造函数可用。

3.4. Step 2 给一种错误类别关联一个值

一个 error_condition 对象包括两个属性:valuecategory,现在我们需要让构造函数正确的初始化。

通过让构造调用 make_error_condition()来实现,为了实现用户可拓展,这个方法通过 ADL(argument-dependent lookup)来定位。默认的,make_error_condition()errc 一样是定义在 namespace std 里。

make_error_condition()的实现很简单:

error_condition make_error_condition(errc e)

{

  return error_condition(

    static_cast<int>(e),

    generic_category());

}

这个构造方法用两个参数构造 error_condition 来显式定义错误值和错误类别。

如果是在转换构造一个 error_code,就用 make_error_code(),在某些方面,error_codeerror_condition 的构造其实是一样的。

3.5. Step 3 显示转换成 errorcode 或者 errorcondition

尽管 error_code 最开始是想用于有操作系统差异性的错误码,可移植性的代码需要用一个 errc 枚举元来构造一个 error_code 错误码。因此,make_error_code(errc)make_error_condition(errc)都被提供了,可移植性的代码可以这样使用它们:

void do_foo(std::error_code & ec)

{

#if defined(_WIN32)

  // Windows implementation...

#elif defined(linux)

  // Linux implementation...

#else

  // do_foo not supported on the platform

  ec = make_error_code(std::errc::not_supported);

#endif

}

3.6. 一点历史

最开始<system_error>提案里把 error_code 常量定义为对象:

extern error_code address_family_not_support;

extern error_code address_in_use;

...

extern error_code value_too_large;

extern error_code wrong_protocol_type;

LWG(Library Working Group 库开发组)担心定义这么多全局对象需要的大小太过头了,要求提供另外一种可行的方法,我们研究过使用 constexpr 的可能性,最终发现和<system_error>这个组件有一些地方不适用,这让使用 enum 成为了最好的选择。

后面,我会继续展示你该如何添加自己的 error codeserror conditions

3.7. 译注:需要补充的坑

  1. type_traits
  2. SFINAE
  3. truetype,falsetype
  4. ADL

这部分翻译有点吃力,理解并不透彻!

4. part4

4.1. 创建你自己的错误码

我在 part one 里就说过,设计<system_error>的原则之一就是要支持用户自定义拓展,就是用户可以用这个工具来描述定义自己的错误码。

这一章里,我将概述一下你应该怎么做。举一个例子,假设你正在写一个 HTTP 库并且需要根据不同的 HTTP 返回的错误码处理的错误。

4.2. Step 1: 定义错误值

你首先需要定义一系列错误值,假设你在使用 C++0x,你可以用 enum class,就像 std::errc 一样:

enum class http_error

{

  continue_request = 100,

  switching_protocols = 101,

  ok = 200,

  ...

  gateway_timeout = 504,

  version_not_supported = 505

};

这些错误根据 HTTP 返回值来指定了不同的数值。很明显也很重要的是,当你使用这些错误码的时候,不要选0作为某个错误码的数值,你应该记得<system_error>有转换:0 = success。

顺带提一句:如果为了兼容 C++03,你也可以去掉 class 关键字。

enum http_error

{

 ...

};

注:C++0x 的 enum classenum 的区别就是前者把枚举元放进了类里,因此你必须要在前面加上类名才能访问它,比如 http_error::ok,你可以近似的认为就像包进了命名空间一样:

namespace http_error

{

  enum http_error_t

  {

    ...

  };

}

后续我会使用 enum class,读者可以自行尝试用命名空间包含。

4.3. Step 2: 定义一个 error_category

一个 error_code 对象包含着类别和值,类别决定了这个数值(比如100)是代表着 http_error::continue_requeststd::errc::network_down(Linux 下是 ENETDOWN)或其他意思。 为了构造一个新类别,你必须继承 error_category 类:

class http_category_impl : public std::error_category

{

public:

  virtual const char * name() const;

  virtual std::string message(int ev) const;

};

目前我们只实现一下继承自 error_category 的虚函数。

4.4. Step 3: 给这个类别一个可读性强的名字

error_category::name()这个虚函数必须返回一个代表类别的字符串:

const char* http_category_impl::name() const

{

  return "http";

}

这个名字不强制要求全局独一无二,因为它只有在把 error code 写入输出流时才被用到。尽管如此,对一个给定的程序而言,有一个独一无二的命名总是好的。

4.5. Step 4: 把 error codes 转换为字符串

error_category()::message()方法把一个错误值转换成对应这个错误值的字符串:

std::string http_category_impl::message(int ev) const

{

  switch (ev)

  {

  case http_error::continue_request:

    return "Continue";

  case http_error::switching_protocols:

    return "Switching protocols";

  case http_error::ok:

    return "OK";

  ...

  case http_error::geteway_timeout:

    return "Gateway time-out";

  case http_error::version_not_supported:

    return "HTTP version not supported";

  default:

    return "Unknow HTTP error";

  }

}

当你调用 error_code::message()时就可以把 error_code 转换成对应的错误信息了。

<system_error>没有对这些消息的本土化(原文用的是 localisation,译者理解就是指不同系统环境下的不同)提供帮助,如果是库函数里的错误,会基于不同的环境给出不同的结果,如果你的程序也需要支持 localisation,我建议你用同样的方法。(一点历史:LWG 意识到过要支持 locallisation,但是由于无法和用户可拓展性协调好,最终选择了在标准中对这方面只字不提)

4.6. Step 5: 类别要唯一

一个继承自 error_category 的对象的唯一性是由其地址决定的,也就是当你这样写:

const std::error_category & cat1 = ...;

const std::error_category & cat2 = ...;

if (cat1 == cat2)

  ...

这里的 if 判断就等价于你这样写:

if (&cat1 == &cat2)

 ...

从这个标准库的例子可以看出,你需要提供一个方法来返回这个类别对象的引用:

const std::error_category & http_category();

这个方法必须始终返回同一个对象,一种实现方法是定义为全局对象:

http_category_impl http_category_instance;



const std::error_category & http_category()

{

  return http_category_impl;

}

然而用一个全局变量会出现在不同模组中使用初始化顺序的问题,另一个可选方案是用静态变量:

const std::error_category & http_category()

{

  static http_category_impl instance;

  return instance;

}

这样这个类的对象就会在第一次使用的时候被初始化好。C++0x 也保证了这个初始化是线程安全的(C++03没有保证)

一点历史:在早期设计阶段,我们考虑过使用整型或者字符串来标志一类 error_code,最主要的问题就是需要保证在和用户可拓展性结合的时候,还要保证独一无二的特性。如果一个类别是用整形或者字符串来定义的,那如何解决两个相关库的冲突?用类来作为标志符,可以用链接器来保证不同的类别会被不一样的识别。以及,用继承基类的方法,可以让我们保持错误码可复制的同时使用多态的特性。

4.7. Step 6: 从枚举里构建一个错误码

如同我在 part3 中所说,<system_error>的实现要求 make_error_code()方法来把一个错误码和类别联系起来。比如说还是 HTTP 错误,你可以像这样写:

std::error_code make_error_code (http_error e)

{

  return std::error_code(static_cast<int>(e), http_category());

}

更完整点,你还可以给错误情况也提供相似的方法:

std::error_condition make_error_condition(http_error e)

{

  return std::error_condition(static_cast<int>(e), http_category());

}

因为<system_error>实现的时候找这些方法都是通过 ADL,你需要把他们和 http_error 类放到同一个命名空间

4.8. Step 7: 注册一个隐式转换到 error_code

因为 http_error 枚举元被用作 error_code 常量,用 is_error_code_enum 模板元来实现一个转换构造:

namespace std

{

  template <>

  struct is_error_code_enum<http_error> : public true_type {};

}

4.9. Step 8: (可选)设置一个默认的错误情况

有些你定义的错误可能和标准库的 errc 错误情况意思相同。比如说,HTTP 应答码403 Forbidden 也基本上和 std::errc::permission_denied 相同

error_category::default_error_condition()虚函数允许你对给定错误码定义等价(equivalent)的错误情况(见 part2 关于等价的定义)。

对于这个 HTTP 错误,你可以这样写:

class http_category_impl : std::error_category

{

public:

  ...

  virtual std::error_condition default_error_condition(int ev) const;

};

...

std::error_condition http_category_impl::default_error_condition(int ev) const

{

  switch (ev)

  {

  case http_error::forbidden;

    return std::errc::permission_denied;

  default:

    return std::error_condition(ev, *this);

  }

}

如果你选者不重载这个虚函数,那么错误码默认的错误情况就是有相同错误值和类别的了(default)

4.10. 使用

你可以把 http_error 枚举元用作 error_code 常量了:

比如处理的同时改变错误码

void server_side_http_handler(..., std::error_code & ec)

{

  ...

  ec = http_error::ok;

}

以及来检验它:

std::error_code ec;

load_resource("http://some/url", ec);

if(ec == http_error::ok)

  ...

有时候错误值基于 HTTP 应答码,我们可以直接用应答码来设置 error_code

std::string load_resource(const std::string & url, std::error_code & ec)

{

  // send request ...



  // receive response ...



  int response_code;

  parse_response(..., &response_code);

  ec.assign(response_code, http_category());



  // ...

}

最后如果你在 Step 8里定义了一个等价关系,那么你可以:

std::error_code ec;

data = load_resource("http://some/url", ec);

if (ec == std::errc::permission_denied)

  ...

原始的错误码就保证了错误没有丢失,方便定位到错误发送的根源。

下一节,我会展示如何使用用户自定义的错误码。

5. part5

5.1. 创建你自己的 error condition

<system_error>组件并不只是在 error_code 上支持用户可拓展,error_condition 也可以自定义。

5.2. 为什么需要自己的 error condition

为了回答这个问题,需要先回顾一下 error_codeerror_condition 的区别:

  • class error_code 代表了一种特定的操作返回的错误值
  • class error_condition 你希望在代码中进行测试的一种情况

这是一些建议使用 error_condition 的情况

  1. 有操作系统差异性的错误的抽象
    • 假设你正在写一个可移植性的方法 getaddrinfo(),暂定两种错误情况:当前无法解析,请稍后再试无法解析,而 getaddrinfo()返回错误又和平台相关
      • 在 POSIX 上,这俩错误码分别是 EAI_AGAINEAI_NONAME,且是在不同命名空间下的 errno 值,意味着你需要实现一个新的 error_category 错误类别来获取这些错误码
      • 而在 Windows 上,这两个错误码分别是 WSAEAI_AGAINWSAEAI_NONAME,尽管名称上和 POSIX 的很像,但是共享 Getlasterror 命名空间,因此你可能想复用 std::system_category()来代表 getaddrinfo()在 Windows 下的错误
    • 为了避免丢掉信息,你可能想在保留原始有平台差异的错误码的的同时提供两种错误情况 error_condition(比方说叫做 name_not_found_try_againname_not_found),这样这个 API 的使用者就可以针对这种情况测试了
  2. 给通用的错误码一个和上下文相关的意思
    • 大多数 POSIX 系统调用用 errno 来反馈错误,许多错误被复用在不同的功能里导致你需要查看相应的具体位置来判断到底是什么错误。如果你用这些系统调用来实现自己的代码,那么对用户来说这些错误就更摸不着头脑了。
    • 比方说:你实现了一个简单的数据库,每一个条目(entry)都被存储在一个单独的文件里,当你试图读文件的时候,数据库调用 open()方法来读取文件,这个方法设置了错误码 ENOENT(if the file does not exist)
    • 因为数据库的存储方法对于用户而言是抽象的,你不可能让用户知道这个意味着 no_such_file_or_directory,事实上你可以创建你自己的富有语义的错误情况 no_such_entry 等效表示 ENOENT
  3. 测试一系列相关的错误
    • 随着你的代码库的增长,你也许发现有一些错误是类似的,也许你需要一个对系统可用资源低的反馈:
      • not_enough_memory
      • resource_unavailable_try_again
      • too_many_files_open
      • too_many_files_open_in_system
    • 在不同的地方可能错误码不一样,但是对这些错误的反应方式都是一样的,所以如果有一个一致的表述:low_system_resources 就可以方便的写如下代码来测试:
if (ec == low_system_resources)

    // do something.

5.3. Step 1 : 定义你自己的错误值

你需要创建一个 enum 枚举类给这些错误码,类似于 std::errc

enum class api_error

{

  low_system_resources = 1,

  ...

  name_not_found,

  ...

  no_such_entry

};

这些值用多少其实不是很重要,只要保证他们各不相同且不为0,默认值0一般表示 success 没有错误。

5.4. Step 2 : 定义一个 error_category

一个 error_condition 对象包含错误值和种类,为了创建一个新类,你必须从 error_category 继承:

class api_category_impl : public std::error_category

{

public:

  virtual const char * name() const;

  virtual std::string message(int ev) const;

  virtual bool equivalent(const std::error_code & ec, int condition) const;

};

5.5. Step 3 : 给这个类别一个可读性强的名字

const char * api_category_impl::name() const

{

  return "api";

}

5.6. Step 4 : 把错误情况转换为字符串

error_category::message()方法把错误值转换为一个表示这个错误的字符串(因此 enum 里的值并不重要):

std::string api_category_impl::message(int ev) const

{

  switch (ev)

  {

  case api_error:low_system_resources:

    return "Low system resources";

  ...

  }

}

当然你可能根本不打算调用这个方法,那么你可以简单的写写:

std::string api_category_impl::message(int ev) const

{

  return "api error";

}

5.7. Step 5 : 实现错误的等价判断

虚函数 error_category::equivalent()被用来定义 errorcode 和 errorcondition 的等价关系,有两种重载方法:

virtual bool equivalent(int code, const error_condition & condition) const;

这种被用来建立当前种类下的 error_code 和任意 error_condition 的一致。

virtual bool equivalent(const error_code & code, int condition) const;

这种建立了当前种类下的 error_condition 和其他种类的 error_code 的等价关系。

因为你在创建自定义的 error_condition,这个方法你必须重载。

定义等价关系很简单,如果你想要一个 error_code 等价你写的错误情况,就 return true,否则 return false

如果你是想抽象一个有系统差异的错误,你就得这样实现:

bool api_category_impl::equivalent(const std::error_code & code, int condition) const

{

  switch (condition)

  {

  ...

  case api_error::name_not_found:

#if defined(_Win32)

    return code == std::error_code(WSAEAI_NONAME, system_category());

#else

    return code == std::error_code(EAI_NONAME, getaddrinfo_category());//很显然 getaddrinfo_category()需要在其它地方定义

  ...

  default:

    return false;

  }

}

你想写多复杂都行,甚至能复用其它 error_condition.

如果你像创建一个语义相关的错误情况或者测试一些相关的错误:

bool api_category_impl::equivalent(const std::error_code & code, int condition) const

{

  switch (condition)

  {

  case api_error::low_system_resources:

    return code == std::errc::not_enough_memory || code == std::errc::resource_unavailable_try_again ||

           code == std::errc::too_many_files_open || code == std::errc::too_many_files_open_in_system;

  case api_error::no_such_entry:

    return code == std::errc::no_such_file_or_directory;

  default:

    return false;

  }

}

5.8. Step 6 : 给种类一个独特的标志

你应该给构造的类一个引用:

const std::error_category & api_category();

为了总是使用同一个引用,你可以定位为全局变量:

api_category_impl api_category_instance;

const std::error_category & api_category()

{

  return api_category_instance;

}

或者用 C++0x 线程安全的静态变量

const std::error_category & api_category()

{

  static api_category_impl instance;

  return instance;

}

5.9. Step 7 : 从枚举里构造一个 error_condition

<system_error>的实现要求一个 make_error_condition()方法来把一个错误值关联到类里:

std::error_condition make_error_condition(api_error e)

{

  return std::error_condition(static_cast<int> e, api_category());

}

为了完整起见,同样还需要给 error_code 一个相似的构造函数,留给读者自己试试

5.10. Step 8 : 注册一个到 error_condition 的隐式转换

最终,为了 api_error 枚举器可以被用作 error_condition 的常量,需要一个 is_error_condition_enum 模板类的转换构造:

namespace std

{

  template <>

  struct is_error_condition_enum<api_error> : public true_type {};

}

5.11. 使用 error_condition

现在 api_error 枚举器可以被用作 error_condition 常量了,就好像和 std::errc 里的一样使用:

std::error_code ec;

load_resource("http://some/url", ec);

if (ec == api_error::low_system_resources)

  ...

就像我前面多次提及的,原始的错误码被保留没有丢失任何信息。不管错误码来自操作系统还是 HTTP 库还是自己的错误目录,你自定义的 error_conditions 都可以很好的匹配