Sta*_*ked 53 c++ error-handling
关于错误处理的大多数建议归结为一些提示和技巧(例如,参见这篇文章).这些提示很有帮助,但我认为他们没有回答所有问题.我觉得我应该根据某种哲学设计我的应用程序,这是一种为构建基础提供坚实基础的思想流派.是否有关于错误处理主题的理论?
这是一些实际问题:
在许多情况下,常识足以开发出足够好的策略来处理错误情况.但是,我想知道是否有更正式/"学术"的方法?
PS:这是一个普遍的问题,但也欢迎C++特定的答案(C++是我工作的主要编程语言).
ROA*_*OAR 14
记录应该只在应用程序代码中完成的东西?或者可以从库代码中进行一些日志记录.
只想对此发表评论.我的观点是永远不要直接登录库代码,但提供钩子或回调来在应用程序代码中实现它,因此应用程序可以决定如何处理日志中的输出(如果有的话).
mar*_*ril 11
几年前我完全想到了同样的问题:)
在搜索和阅读了几件事之后,我认为我发现的最有趣的参考是Andy Longshaw和Eoin Woods 的错误生成,处理和管理模式.这是一个简短而系统的尝试,涵盖了你提到的基本习语和其他一些习惯用语.
这些问题的答案很有争议,但上面的作者足够勇敢在会议中揭露自己,然后把他们的想法写在纸上.
War*_*Dew 10
为了理解错误处理需要做什么,我认为需要清楚地理解遇到的错误类型以及遇到错误的上下文.
对我来说,将两种主要类型的错误视为非常有用:
应该永远不会发生的错误,通常是由于代码中的错误.
在正常操作中预期和无法阻止的错误,例如由于应用程序无法控制的数据库问题而导致数据库连接中断.
应该处理错误的方式在很大程度上取决于它是哪种类型的错误.
影响应如何处理错误的不同上下文是:
应用代码
图书馆代码
库代码中的错误处理与应用程序代码中的处理有些不同.
下面讨论处理两种主要类型错误的原理.还解决了库代码的特殊注意事项.最后,原始文章中的具体实际问题将在所提出的哲学背景下得到解决.
许多错误是编程错误的结果.这些错误通常无法纠正,因为无法预料到特定的编程错误.这意味着我们无法事先知道错误离开应用程序的条件,因此我们无法从该条件中恢复而不应该尝试.
最终,修复此类错误的方法是修复编程错误.为方便起见,应尽快浮出错误.理想情况下,程序应在识别出此类错误并提供相关信息后立即退出.快速而明显的退出减少了完成调试和重新测试周期所需的时间,允许在相同的测试时间内修复更多错误; 这反过来导致在部署时产生更强大的应用程序并减少错误.
处理此类错误的另一个主要目标应该是提供足够的信息以便于识别错误.例如,在Java中,抛出RuntimeException通常会在堆栈跟踪中提供足够的信息以立即识别错误; 在干净的代码中,通常可以通过检查堆栈跟踪来识别即时修复.在其他语言中,可以记录调用堆栈或以其他方式保留必要的信息.为了简洁起见,不要压制信息是至关重要的; 在发生此类错误时,不要担心会占用多少日志空间.提供的信息越多,错误修复得越快,并且当应用程序进入生产状态时,污染日志的错误将越少.
现在,在某些服务器应用程序中,即使遇到偶然的编程错误,服务器也必须具有足够的容错能力才能继续运行.在这种情况下,最好的方法是在必须继续操作的服务器代码和可以允许失败的任务处理代码之间进行非常明确的分离.例如,任务可以降级为线程或子进程,就像许多Web服务器中所做的那样.
在这样的服务器体系结构中,处理任务的线程或子进程可以被视为可能失败的应用程序.上述所有考虑因素都适用于这样的任务:应该通过干净的退出任务尽快显示错误,并且应记录足够的信息以允许容易地找到并修复错误.例如,当这样的任务在Java中退出时,通常应记录导致退出的任何RuntimeException的整个堆栈跟踪.
尽可能多的代码应该在处理任务的线程或进程中执行,而不是在主服务器线程或进程中执行.这是因为主服务器线程或进程中的任何错误仍会导致整个服务器出现故障.最好将代码 - 包含它的bug - 推送到任务处理代码中,当bug出现时,它不会导致服务器崩溃.
在正常操作中预期和无法防止的错误,例如数据库或与应用程序分开的其他服务的异常,需要非常不同的处理.在这些情况下,目标不是修复代码,而是让代码在有意义时处理错误,并告知可以解决问题的用户或操作员.
例如,在这些情况下,应用程序可能希望丢弃迄今为止累积的任何结果,并重试该操作.在数据库访问中,使用事务可以帮助确保丢弃累积的数据.在其他情况下,编写一个包含此类重试的代码会很有用.幂等性的概念在这里也很有用.
当自动重试不能充分解决问题时,应该告知人类.应通知用户操作失败; 通常可以为用户提供重试选项.然后,用户可以判断是否需要重试,并且还可以对输入进行改变,这可以帮助事情在重试时更好.
对于此类错误,可以使用日志记录和电子邮件通知来通知系统操作员.与编程错误的记录不同,正常操作中预期的错误记录应该更简洁,因为错误可能会多次发生并在日志中出现多次; 操作员通常会分析许多错误的模式,而不是关注一个单独的错误.
以上对错误类型的讨论直接适用于应用程序代码.错误处理的另一个主要上下文是库代码.库代码仍然具有相同的两种基本类型的错误,但它通常不能或不应该直接与用户通信,并且它对应用程序上下文的了解较少,包括是否可以立即退出,而不是应用程序代码.
因此,库应该如何处理日志记录,它们应该如何处理正常操作中可能出现的错误,以及它们应该如何处理编程错误和其他永远不会发生的错误.
关于日志记录,库应尽可能支持以客户端应用程序代码所需的格式进行日志记录.一种有效的方法是根本不进行日志记录,并允许应用程序代码根据库提供给应用程序代码的错误信息进行所有日志记录.另一种方法是使用可配置的日志记录接口,允许客户端应用程序提供日志记录的实现,例如在首次加载库时.例如,在Java中,库可能使用logback日志记录接口,并允许应用程序担心要配置哪些日志记录实现以使用回溯.
对于不应该发生的错误和其他错误,库仍然不能简单地退出应用程序,因为这可能是应用程序无法接受的.相反,库应该退出库调用,为调用者提供足够的信息来帮助诊断问题.可以使用堆栈跟踪以异常的形式提供信息,或者如果正在使用可配置的日志记录方法,则库可以记录信息.然后,应用程序可以将此处理为与此类型的任何其他错误一样,通常是通过退出或在服务器中,通过允许任务进程或线程退出,使用与编程错误相同的日志记录或错误报告.应用程序代码.
在正常操作中预期的错误也应该报告给客户端代码.在这种情况下,与在客户端代码中遇到的这种类型的错误一样,与错误相关联的信息可以更简洁.通常,库应该对此类错误进行较少的本地处理,更多地依赖于客户端代码来决定是否重试以及重试多少次.然后,如果需要,客户端代码可以将重试决定传递给用户.
现在我们有了这个哲学,让我们将它应用到你提到的实际问题中.
如果是正常操作中出现的错误,请重试或可能在本地查询用户.否则,将其传播到更高级别的代码.
如果它是正常操作中预期的错误,并且用户输入将有助于确定要采取的操作,请获取用户输入并记录简洁消息; 如果它似乎是编程错误,请向用户提供简要通知并记录更多信息.
从库代码中记录应该在客户端代码的控制之下.最多,库应该登录到客户端提供实现的接口.
可以在本地捕获正常操作中预期的异常,并重试或以其他方式处理操作.在所有其他情况下,应允许例外传播.
第三方库中的错误类型与应用程序代码中出现的错误类型相同.错误应主要根据它们所代表的错误类型进行处理,并对库代码进行相关调整.
应用程序代码应提供编程错误时的错误的完整描述,以及在正常操作中可能发生的错误的简洁描述; 在任何一种情况下,描述通常比错误代码更合适.库可以提供错误代码作为描述错误是编程还是其他内部错误的方式,或错误是否是在正常操作中可能发生的错误,后者类型可能更精细地细分; 但是,异常层次结构可能比语言中的错误代码更有用.请注意,从命令行运行的应用程序可以充当shell脚本的库.
免责声明:我不知道任何关于错误处理的理论,但是,当我探索各种语言和编程范例,以及玩弄编程语言设计(并讨论它们)时,我确实重复思考过这个问题.因此,接下来是对我迄今为止的经验的总结; 客观论点.
注意:这应该涵盖所有问题,但我甚至没有尝试按顺序解决它们,而是更喜欢结构化的演示文稿.在每个部分的最后,为了清楚起见,我对它回答的那些问题给出了简洁的答案.
介绍
作为前提,我想指出,无论需要讨论什么,在设计库(或可重用代码)时都必须牢记一些参数.
作者无法理解如何使用这个库,因此应该避免使集成更加困难的策略.最明显的缺陷是依赖于全球共享的状态; 线程本地共享状态也可能是与协同程序/绿线程交互的噩梦.使用这样的协同程序和线程也强调同步最好留给用户,在单线程代码中它将意味着没有(最佳性能),而在协同程序和绿线程中用户最适合实现(或使用现有的) ()专用同步机制的实现.
话虽这么说,当库仅供内部使用时,全局或线程局部变量可能很方便; 如果使用,应将其明确记录为技术限制.
记录
记录消息的方法有很多种:
作为库的作者,日志应该集成在客户端基础结构中(或关闭).最好的方法是允许客户端提供钩子以便自己处理日志,我的建议是:
stdout和stderr(取决于严重性),直到客户端明确表示不记录我要注意的是,按照引言中描述的指南,同步留给了客户端.
关于是否记录错误:不要记录(作为错误)您通过API报告的内容; 但是,您仍然可以以较低的严重性记录详细信息.客户端可以在处理错误时决定是否报告,例如,如果这只是一个推测性呼叫,则选择不报告.
注意:某些信息不应该包含在日志中,而其他一些信息最好是混淆的.例如,不应记录密码,并且信用卡或护照/社会安全号码最好被混淆(至少部分).在为这种敏感信息设计的库中,这可以在日志记录期间完成; 否则申请应该照顾这个.
记录应该只在应用程序代码中完成的东西?或者可以从库代码中进行一些日志记录.
应用程序代码应决定策略.库是否记录取决于是否需要.
发生错误后继续?
在我们实际谈论报告错误之前,我们应该问的第一个问题是是否应该报告错误(处理错误),或者如果出现问题,那么中止当前流程显然是最好的策略.
这当然是一个棘手的话题.一般来说,我建议设计这样一个选项,如有必要,可以进行清除/重置.如果在某些情况下无法实现这一目标,那么这些案件应该引起流程的堕胎.
注意:在某些系统上,可以获取进程的内存转储.如果应用程序处理敏感数据(密码,信用卡,护照......),最好在生产中停用(但可以在开发期间使用).
注意:有一个调试开关可以将一部分错误报告调用转换为带有内存转储的流产,以协助开发期间的调试.
报告错误
出现错误表示无法满足功能/接口的合同.这有几个后果:
后一点将在稍后处理; 现在让我们专注于报告错误.客户不应该无意中忽略此报告.这就是为什么使用错误代码是如此可憎(在语言中可以忽略返回值):
ErrorStatus_t doit(Input const* input, Output* output);
Run Code Online (Sandbox Code Playgroud)
我知道需要在客户端部分采取明确行动的两种方案:
optional<T>,either<T, U>,...)前者是众所周知的,后者非常多地用于函数式语言,并且在std::future<T>其他实现存在的情况下以C++ 11的形式引入.
我建议在可能的情况下更喜欢后者,因为它更容易理解,但在没有预期结果的情况下恢复为异常.对比:
Option<Value&> find(Key const&);
void updateName(Client::Id id, Client::Name name);
Run Code Online (Sandbox Code Playgroud)
在"只写"操作的情况下,例如updateName,客户端不能使用结果.它可以介绍,但很容易忘记检查.
当结果类型不切实际或不足以传达细节时,也会发生恢复异常:
Option<Value&> compute(RepositoryInterface&, Details...);
Run Code Online (Sandbox Code Playgroud)
在这种外部定义的回调的情况下,存在几乎无限的潜在故障列表.在这种情况下,实现可以使用网络,数据库,文件系统......以便准确报告错误:
目标是让这个异常冒泡到决定接口实现的层(至少),因为只有在这个级别才有机会正确解释抛出的异常.
注意:外部定义的回调不会被强制使用异常,我们应该期望它可能会使用一些异常.
使用错误
为了使用错误报告,客户需要足够的信息来做出决定.结构化信息(例如错误代码或异常类型)应该是首选(对于自动操作),并且可以以非结构化方式提供附加信息(消息,堆栈......)(供人类调查).
最好是一个功能清楚地记录所有可能的故障模式:何时发生以及如何报告.但是,特别是在执行任意代码的情况下,客户端应该准备好处理未知的代码/异常.
当然,一个值得注意的例外是结果类型:boost::variant<Output, Error0, Error1, ...>提供编译器检查的已知故障模式的详尽列表......当然,返回此类型的函数仍然可以抛出.
如何确定记录错误或将其显示为错误消息给用户?
当订单无法满足时,应始终警告用户,但应显示用户友好(可理解)的消息.如果可能,还应提供建议或解决方法.详细信息适用于调查团队.
从错误中恢复?
最后,但肯定不是最不重要的,是关于错误的真正令人恐惧的部分:恢复.
这是数据库(真实的)非常适合的事情:类似事务的语义.如果发生任何意外情况,则事务中止,就像没有发生任何事情一样.
在现实世界中,事情并不简单.取消电子邮件的简单例子让人想起:太晚了.可能存在协议,具体取决于您的应用程序域,但这不在讨论范围之内.但是,第一步是恢复内存状态的能力; 在大多数语言中,这远非简单(STM今天只能这么做).
首先,说明挑战:
void update(Client& client, Client::Name name, Client::Address address) {
client.update(std::move(name));
client.update(std::move(address)); // Throws
}
Run Code Online (Sandbox Code Playgroud)
现在,更新地址失败后,我留下了半更新client.我能做什么 ?
在任何情况下,所需的簿记都会导致错误蔓延.
最糟糕的是:没有关于腐败程度的安全假设(除了client现在已经拙劣).或者至少,没有假设会忍受时间(和代码更改).
通常,获胜的唯一方法就是不玩.
可能的解决方案:交易
只要有可能,关键的想法是定义宏函数,这些函数将失败或产生预期结果.那是我们的交易.他们的形式是不变的:
Either<Output, Error> doit(Input const&);
// or
Output doit(Input const&); // throw in case of error
Run Code Online (Sandbox Code Playgroud)
事务不会修改任何外部状态,因此如果它无法生成结果:
任何不是事务的函数都应该被认为已经损坏了它触及的任何东西,因此处理非事务函数错误的唯一理智方法是让它冒泡直到达到事务层.任何先前处理错误的尝试最终都注定要失败.
如何判断错误是应该在本地处理还是传播到更高级别的代码?
如果出现例外情况,您通常应该在哪里捕获它们?在低级别或更高级别的代码?
只要可以安全地处理它们,并且这样做是有价值的.最值得注意的是,可以捕获错误,检查是否可以在本地处理,然后处理或传递它.
您是否应该通过所有代码层争取统一的错误处理策略,或者尝试开发一个能够适应各种错误处理策略的系统(以便能够处理来自第三方库的错误).
我以前没有解决过这个问题,但是我认为很明显,我强调的方法已经是双重的,因为它既包含结果类型也包含异常.因此,处理第三方库应该是一个很好的,尽管我建议其他原因包装它们(第三方代码除了负责阻抗适应的面向业务的接口之外更好地隔离).
我对库代码中的日志记录(或其他操作)的看法是绝对的.
库不应对其用户施加策略,并且用户可能会发生意外错误.也许该程序故意征求特定错误,期望它到达,以测试某些条件.记录此错误会产生误导.
记录(或其他任何)对调用者施加策略,这是不好的.此外,如果一个无害的错误条件(例如,调用者将无视或无理地重试)会以高频率发生,则日志量可能会掩盖任何合法错误或导致稳健性问题(填充光盘,使用过多的IO)等等)