你自己的error_code

更新时间:2023-06-30 05:14:37 阅读: 评论:0

你⾃⼰的error_code
本系列⽂章翻译⾃ ,原⽂。
翻译⼯作已获原作者授权!
系列⽂章⽬录古北岳
⼀、你⾃⼰的 error_code
⼆、你⾃⼰的 error_condition
三、⾼效地使⽤ error_code
四、关于 error_code 的⼀些澄清
⽂章⽬录
前⾔
最近我在⽤std::error_code给⾃⼰的应⽤实现“错误状态分类”功能,这⾥分享⼀下我的⼀些经验和见解。
C++11 提供了⼀个⾮常精妙的错误状态分类机制,你也许已经见过⼀些相关的名词,⽐如“error code”、“error condition”、“error category”,但要搞清楚它们是⼲嘛的以及该怎么使⽤它们还是有点困难的。⽹上⽐较有价值的资料是 库的作者 Christopher Kohlhoff 所写的系列⽂章:
这些⽂章已经写的很棒了,但我觉得能够提供更多的信息、提供另⼀种理解问题的思路,也是⼀件不错的事情,所以我们开始吧…
问题
⾸先,为什么我们需要错误码。假设我有⼀个航班查询系统,你告诉我从哪⾥出发到哪⾥,我就告诉你具体的航班以及价格。为了实现这个功能,我的系统需要调⽤另外两个服务:
⼀个查询并返回符合你要求的航班列表
⼀个查询这个列表中的航班是否有空余的座位(以及是经济舱还是商务舱)
这⾥的每个服务都可能会因为很多原因⽽失败(产⽣错误),我们可以枚举失败的原因(每个服务都不⼀样)。⽐如,实现这两个服务的开发者们选择了这些枚举值:
enum class FlightsErrc
{
虫草花
// no 0
NonexistentLocations =10,// requested airport doesn't exist
DatesInThePast,// booking flight for yesterday
InvertedDates,// returning before departure
NoFlightsFound      =20,// did not find any combination
ProtocolViolation    =30,// e.g., bad XML
ConnectionError,// could not connect to rver
ResourceError,// rvice run short of resources
Timeout,// did not respond in time
};
InvalidRequest =1,// e.g., bad XML
CouldNotConnect,// could not connect to rver
InternalError,// rvice run short of resources
NoRespon,// did not respond in time
NonexistentClass,// requested class does not exist
NoSeatAvailable,// all ats booked
};
可以看到,⾸先不同服务中错误的原因看起来很相像,但它们却被分配了不同的名字和数值(错误码),这是因为两个服务是由两个不同的团队独⽴开发的。⽽这也意味着同⼀个错误码可以表⽰两个完全不同的错误状态,取决于这个错误码是由哪个服务上报的。
其次,从枚举名中可以看出,导致错误的原因有如下⼏个不同来源:
环境:服务内部的问题(⽐如资源问题)
通信:服务之间的通信
⽤户:在请求中提供了错误的数据
只是运⽓不好:其实算不上错误,但⽆法给⽤户响应(⽐如所有座位都被订了)
那么,我们到底为什么需要这些不同的错误码?当这其中任意⼀个错误发⽣时,我们会停⽌处理⽤户当前的请求,并给予⼀定反馈(帮助⽤户进⼀步操作)。如果系统⽆法给⽤户提供其请求的航班,我们想告诉⽤户以下⼏种情况:
1. 你的请求⽆效
2. 没有任何已知的航线符合你的旅⾏需求
3. 系统内部出现了⼀些你⽆法理解的问题,这些问题导致我们⽆法给你请求的结果
另⼀⽅⾯,为了内部审查或者排查 bug,我们需要更详细的信息写⼊到⽇志中,⽐如错误是哪个系统上报的,及其产⽣的详细原因。这些可以编码成⼀个整形数字,其它更多的信息(⽐如我们想连接的是哪个机站,或者我们尝试接⼊的是哪个数据库)可以被分别记录在⽇志中,所以⽤整形数字编码错误类型已经⾜够了。
std::error_code
C++ 标准库中的std::error_code正是被设计来表⽰这样的信息:⼀个数字代指状态,加上这个数字表⽰的含义所在的“域”。换⽽⾔
之,std::error_code是⼀个元素对(pair):{int, domain},这体现在它的接⼝中:
void inspect(std::error_code ec)
{
ec.value();// the int value
ec.category();// the domain
}
但你基本上不会⽤这种⽅式来检验⼀个 error_code。就像之前所述,我们想做的是两件事:将最原始的 error_code(未被后续的上层应⽤解析)记录下来;以及⽤它来回答特定的问题,⽐如“这个错误是不是因为⽤户提供了他明知有误的信息导致的”。
如果你问,为什么要⽤std::error_code⽽不是异常(exception)呢?我得在此澄清⼀下:这两者并不是相互排斥的。我会在程序中使⽤异常来报告错误,⽽异常内部会包含⼀个 error_code,⽤于⽅便地检验错误信息(⽽不是通过储存和解析字符串来实现)。std::error_code跟避免异常⼀点关系都没有!另外,我并不觉得程序中很需要特别多的异常类型,往往⼀个类型就够了:我会在⼀个(或两个)地⽅统⼀ catch
它们,然后检验 error_code 来区分不同的错误状态。
译者注:std::filesystem就是这样使⽤ error_code 的,其函数提供两个版本:⼀个将错误码作为函数参数返回;另⼀个在出现错误时将错误码作为异常抛出。参见 。
接⼊⾃定义枚举
现在我们要调整⼀下std::error_code,让它能够存放前述 Flights 服务的各种错误状态:
NonexistentLocations =10,// requested airport doesn't exist
DatesInThePast,// booking flight for yesterday
InvertedDates,// returning before departure
NoFlightsFound      =20,// did not find any combination
ProtocolViolation    =30,// e.g., bad XML
ConnectionError,// could not connect to rver
ResourceError,// rvice run short of resources
Timeout,// did not respond in time
};
我们需要能够将⾃⼰的枚举值转化为std::error_code:
std::error_code ec = FlightsErrc::NonexistentLocations;
这⾥要注意的是,我们的枚举值必须满⾜⼀个条件:数值 0 不能⽤来表⽰错误状态。0 在任何错误域(类别)中都是⽤来表⽰“成
功”(⽆错误)的,这⼀特例在之后我们检验std::error_code的时候能够利⽤上:
void inspect(std::error_code ec)
{
if(ec)// equivalent to: ec.value() != 0
handle_failure(ec);
圣诞节英语作文el
handle_success();
人人bt}
从这个意义上来说,使⽤数值 200 表⽰成功其实是不对的。
所以我们的FlightsErrc枚举值不从 0 开始。这同时也导致我们可以定义不属于任意⼀个枚举元素的枚举值:
FlightsErrc fe {};
这是 C++ 枚举类型(甚⾄ C++11 中的强类型枚举 enum class)的⼀个重要性质:你能够在枚举元素定义的范围外创建数值。正因为如此,编译器会在switch语句中报出“不是所有的控制路径都返回值(n
ot all control paths return value)”的警告,即使你给所有的枚举元素都写了ca。
回到错误码的转换上,std::error_code有⼀个转换构造模板函数,看起来就像这样:
template<class Errc>
requires is_error_code<Errc>::value
error_code(Errc e)noexcept
: error_code{make_error_code(e)}
{}
(当然,我⽤了⼀个还不存在的 concepts 语法,但你可以 get 到我的意思:只有当std::is_error_code<Errc>::value为true的时候,这个构造函数才可⽤)
译者注:concepts 语法由 C++20 标准引⼊,本博⽂原⽂发布于 2017 年,当时还没有 C++20 标准。
该构造函数就是⼀个能够将⾃定义枚举接⼊ error_code 系统的“定制钩⼦(customization hook)”。想要接⼊FlightsErrc,我们需要确保:
1. std::is_error_code<FlightsErrc>::value返回true;
2. 以FlightsErrc为输⼊参数的make_error_code()函数被定义并且能够被访问。
关于第⼀点,我们需要指定⼀个标准类型特性:
namespace std
{
template<>
struct is_error_code_enum<FlightsErrc>: true_type {};
}
这是在std命名空间中进⾏声明的合法情况之⼀。
关于第⼆点,我们只需要在枚举类型FlightsErrc相同的命名空间中重载make_error_code函数即可:
enum class FlightsErrc;
std::error_code make_error_code(FlightsErrc);
这些是程序或者库的其它部分需要看到内容,因此我们必须把它们放在头⽂件中。⾄于make_error_code函数的具体实现,我们可以放在另⼀个独⽴的翻译单元(cpp ⽂件)中。
完成以上内容后,我们便可以认为FlightsErrc就是⼀种error_code啦:
std::error_code ec = FlightsErrc::NoFlightsFound;
asrt (ec == FlightsErrc::NoFlightsFound);
asrt (ec != FlightsErrc::InvertedDates);
定义错误类别
⾄此,我还只说过error_code是⼀个元素对:{number, domain},其中第⼀个元素在某个域中唯⼀确定了⼀种错误情况,第⼆个元素则在所有可能的错误域中唯⼀确定了⼀个域。问题是,域的唯⼀标识(ID)需要⽤⼀个机器单词来存放,我们怎么才能确保它在现有以及未来的所有库中都是独⼀⽆⼆的呢?我们将域 ID 作为⼀个实现细节隐藏了起来,如果我们想使⽤另⼀个有着⾃⼰的错误枚举的第三⽅库,怎么才能确保它们的域 ID 与我们不同?
std::error_code选⽤的解决⽅法是基于这样的事实:每⼀个全局对象(或者⽤⼀种更正式的⽅式来说:命名空间作⽤域内的对象)都被分配了⼀个独⼀⽆⼆的地址。不管将多少库组合在⼀起,不管有多少全局对象,每个全局对象都有⼀个独特的地址 —— 这很显然。
为了利⽤这⼀点,我们可以将每个想要插⼊到 error_code 系统中的类型与⼀个独特的全局对象关联起来,⽤该对象的地址作为 ID。也就是说,可以使⽤⼀个指针来代表域,⽽这正是std::error_code所采⽤的⽅法。现在的问题是,我们⽤作 ID 的这个指针T*具体是什么⼀种对象T?有⼀个很聪明的选择:我们⽤⼀种能够提供额外好处的类型。具体使⽤的类型T是std::error_category,其额外的好处体现在它的接⼝中:
class error_category {
public:
virtual const char*name()const noexcept=0;
virtual string message(int ev)const=0;
// other members ...
};
我在之前⽤了⼀个叫“域(domain)”的名字,⽽标准库称其为“错误类别”,表达的是同⼀个意思。
它有纯虚成员函数,这暗⽰我们⽤对象指针作域 ID 的类需要从std::error_category派⽣⽽来,⽽且每⼀个错误枚举类型都需要
从std::error_category派⽣⼀个新的相关的类。通常,有着纯虚函数的类意味着在堆上创建对象,但我们不这么做。我们要创建全局对象,然后⽤指针指向它们。
译者注:带有纯虚函数的类⼜叫抽象类,本⾝⽆法实例化。很常见的⼀种⽤法是:在抽象类中定义⼀系列接⼝(纯虚函数),在其派⽣类中实现这些接⼝,然后具体⽤的时候 new ⼀个派⽣类对象(堆上),通过基类的指针指向派⽣类并访问相关函数。这就是为什么作者说有纯虚函数通常意味着在堆上创建对象。
std::error_category中还有⼀些别的虚函数,这些函数在其它有些情况下需要定制(重载),但我们的⽬的只是把 FlightsErrc 插⼊到
error_code 系统中,所以不需要考虑它们。
现在,对于每⼀个派⽣⾃std::error_category⽤于表⽰错误域的类,我们需要提供两个成员函数。函数 name 应该返回⼀个标识这个错误类别(域)的名称。函数 message 则对该错误域中每个枚举值指定
⼀个⽂本描述。为了更好地阐述,我们给枚举类型 FlightsErrc 定义⼀个错误类别。记住:这个从std::error_category派⽣⽽来的类只需要在⼀个翻译单元中可见即可,其它⽂件中我们只需要⽤到它的⼀个实例的地址。
namespace{// anonymous namespace
struct FlightsErrCategory : std::error_category
{
const char*name()const noexcept override;
std::string message(int ev)const override;
};
const char* FlightsErrCategory::name()const noexcept
{
return"flights";
}
std::string FlightsErrCategory::message(int ev)const
{
switch(static_cast<FlightsErrc>(ev))
{
ca FlightsErrc::NonexistentLocations:
return"nonexistent airport name in request";
ca FlightsErrc::DatesInThePast:
return"request for a date from the past";
ca FlightsErrc::InvertedDates:
return"requested flight return date before departure date";
红孩儿大话火焰山
ca FlightsErrc::NoFlightsFound:
return"no filight combination found";
ca FlightsErrc::ProtocolViolation:
return"received malformed request";
ca FlightsErrc::ConnectionError:
return"could not connect to rver";
ca FlightsErrc::ResourceError:
return"insufficient resources";
ca FlightsErrc::Timeout:
return"processing timed out";
default:
杨绵绵
return"(unrecognized error)";
}
}
const FlightsErrCategory theFlightsErrCategory {};雁过留声
}
函数name()返回错误类别的名称,可以输出到⼀些⽐如⽇志之类的东西中:它能帮助你定位错误的原因。这对于不同的错误枚举并不要求独⼀⽆⼆,但不恰当的名称可能导致⽇志⽂件的歧义。
函数message()为错误类别中的每个枚举值提供了⼀段描述信息。它在你调试或者浏览⽇志时很有⽤;但你或许并不想将它未经处理就提供给⽤户。这些信息跟我们在⼀开始写在FlightsErrc定义中的注释很接近。
message()通常是被间接调⽤的,调⽤者⽆法知道错误码数值是⼀个FlightsErrc,所以我们需要显式的将其转化为FlightsErrc。我敢肯定因为遗漏了static_cast⽽⽆法编译。经过转化之后,还存在被检验的数值不在枚举序列中的风险,因此我们需要default标签。(有趣的是,每当我在程序中⽤到enum class,我就⽴刻发现⾃⼰需要⽤static_cast将其与int来回转化)
如何撩男生
最后可以注意到,我们初始化了⼀个全局的FlightsErrCategory对象,这是整个程序中唯⼀⼀个该类型的对象。我们需要它的地址(来区分不同错误类别中的error_code),同时也会⽤到它的多态性质。
虽然类std::error_category不是⼀个字⾯类型 (literal type),但它有⼀个constexpr属性的默认构造函数。⽽FlightsErrCategory的隐式声明的默认构造函数也继承了这个constexpr属性,所以它的全局对象是常量初始化(constant initialization),参见,因此能够避免 。
⾄此,最后缺少的部分只剩下make_error_code()函数的实现:
std::error_code make_error_code(FlightsErrc e)
{
return{static_cast<int>(e), theFlightsErrCategory};
}
⼤功告成,我们的FlightsErrc可以被当成std::error_code来⽤了:

本文发布于:2023-06-30 05:14:37,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/82/1069601.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:错误   需要   函数   提供
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图