异常和错误代码:以正确的方式混合它们

Exceptions and error codes: mixing them the right way

本文关键字:方式 混合 错误代码 异常      更新时间:2023-10-16

我正在开发一个C++加密狗通信库。该库将提供一个统一的接口,用于与SenseLock、KEYLOK、Guardant code等一系列远程代码执行软件狗进行通信。

加密狗基于智能卡技术,具有内部文件系统和RAM。

典型的操作例程包括(1)连接到USB端口的加密狗的枚举,(2)连接到选定的加密狗,(3)执行传递输入和收集输出数据的命名模块。

好吧,所有这些阶段都可能以错误告终,这是微不足道的。可能有很多情况,但最常见的是:

  • 未找到加密狗(肯定是致命案例)
  • 加密狗连接失败(致命案例)
  • 在加密狗(?)中找不到指定的执行模块
  • 由于超时(?),请求的操作失败
  • 请求的操作需要授权(我想这是一种可恢复的情况)
  • 执行加密狗中的模块时发生内存错误(肯定是致命的情况)
  • 加密狗中发生了文件系统错误(肯定是致命的情况)

?-我还不知道这个病例是否被认为是致命的

我仍在决定是抛出异常,还是返回错误代码,或者为这两种情况实现一个方法。

问题是:

  1. 异常是否完全替换了错误代码,或者我只需要将它们用于"致命情况">
  2. 混合使用两种范式(异常和错误代码)是个好主意吗
  3. 为用户提供两个概念是个好主意吗
  4. 有没有关于异常和错误代码混合概念的好例子
  5. 你将如何实现这一点

更新1

如果能从不同的角度看到更多的意见,那会很有趣,所以我决定在这个问题上增加100个声誉奖励。

如果我们谈论的是C++应用程序/模块内部错误处理策略,那么我的个人意见是:

Q:异常是否完全替换错误代码,或者我可能只需要将它们用于";致命病例";?

A:确实如此。对于C++函数来说,异常总是比返回错误代码更好。原因解释如下。

Q:混合使用两种范式(异常和错误代码)是个好主意吗?

A:否。混合很难看,会导致错误处理不一致。当我被迫使用错误修复API(如Win32 API、POSIX等)时,我使用异常抛出包装器。

Q:为用户提供两个概念是个好主意吗?

A:否。用户不清楚该选择哪种变体,通常会做出混合两者的最坏决定。一些用户更喜欢异常,而另一些用户则更喜欢返回错误,如果他们都在同一个项目上工作,就会使项目的错误处理实践变得一团糟。

Q:有没有关于异常和错误代码混合概念的好例子?

A:没有。如果你找到了,就给我看看。IMO在编写C++代码时,如果必须使用错误返回函数(通常必须使用它们),则使用异常抛出包装器隔离错误返回函数是最佳实践。

问:您将如何实现这一点?

A:在C++代码中,我只会使用异常。只有在成功的情况下,我才会回归。错误返回实践严重扰乱了多级错误检查分支的代码,或者更典型甚至更糟的是,错误状态检查缺失,因此错误状态被忽略,这使得代码中充满了难以找到的隐藏错误。

异常使得错误传播不可避免,错误处理被孤立。如果您需要就地处理某种错误,这通常意味着它根本不是错误,而是一些合法的事件,可以通过成功返回报告,并指示一些特定状态(通过返回值或其他方式)。如果你真的需要检查是否在本地发生了一些错误(不是从根try/catch块),你可以在本地try/catch,所以只使用异常不会以任何方式限制你的能力。

重要提示:

对于每种特定的情况,正确定义什么是错误,什么不是错误是非常重要的,以获得最佳可用性。

例如,假设我们有一个函数显示输入对话框并返回用户输入的文本,如果用户可以取消输入,那么取消事件就是成功——不是错误(但必须在返回时以某种方式指示用户取消了输入),而是缺乏资源(如内存或GDI对象或其他东西),或者缺乏显示对话框的显示设备确实是错误。

一般情况下:

异常是C++语言中更自然的错误处理机制。因此,如果您正在开发仅由C++应用程序使用的C++应用程序或库(而不是由C应用程序等使用),那么使用异常是一个好主意。错误返回是一种更便携的方法——你可以将错误代码返回到用任何编程语言编写的应用程序中,甚至可以在不同的计算机上运行。当然,通常操作系统API例程通过错误代码报告它们的状态——使它们依赖于语言是很自然的。因此,您必须在日常编程中处理错误代码BUTIMO计划C++应用程序的错误处理策略基于错误代码只是在找麻烦——应用程序变得完全无法读取。IMO在C++应用程序中处理状态代码的最好方法是使用C++包装器函数/类/方法来调用错误返回功能,如果返回了错误,则抛出异常(所有状态信息都嵌入到异常类中)。

一些重要注意事项和注意事项:

为了在项目中使用异常作为错误处理策略,有一个严格的编写异常安全代码的策略是很重要的。这基本上意味着每个资源都是在某个类的构造函数中获取的,更重要的是在析构函数中释放的-这可以确保不会发生资源泄漏。此外,您还必须在某个地方捕获异常,通常是在根级函数中,如main或窗口过程或线程过程等。

考虑这个代码:

SomeType* p = new SomeType;
some_list.push_back(p);
/* some_list is a sequence of raw pointers so each must be delete-ed
after removing it from this list and when clearing the list */

这是典型的潜在内存泄漏——如果push_back抛出异常,那么动态分配和构造的SomeType对象就会泄漏。

例外安全等价物是:

C++2011及更高版本:

std::unique_ptr<SomeType> pa( new SomeType );
some_list.push_back(pa.get());
pa.release();
/* some_list is a sequence of raw pointers so each must be delete-ed
after removing it from this list and when clearing the list */

过时的C++1998/C++2003变体:

std::auto_ptr<SomeType> pa( new SomeType );
some_list.push_back(pa.get());
pa.release();
/* some_list is a sequence of raw pointers so each must be delete-ed
after removing it from this list and when clearing the list */

但实际上存储原始指针是一种糟糕的、异常的、不安全的做法,因此更合适的C++2011和更高版本是:

std::shared_ptr<SomeType> pa = std::make_shared<SomeType>(); //OR std::unique_ptr
some_list.push_back(pa);
/* some_list is a sequence of smart pointer objects
so everything is delete-ed automatically */

(在C++2003中,在您可以使用boost::shared_ptr或您自己正确设计的智能指针之前)

如果您使用C++标准模板、分配器等,您要么编写异常安全代码(如果您尝试/捕获每一个STL调用,代码就会变得一团糟),要么让代码充满潜在的资源泄漏(不幸的是,这种情况经常发生)。编写良好的C++应用程序总是异常安全的。时期

有什么好的例子可以说明异常和错误代码混合的概念吗?

是的,boost.asio是C++中用于网络和串行通信的无处不在的库,几乎每个函数都有两个版本:异常抛出和错误返回。

例如,iterator resolve(const query&)在失败时抛出boost::system::system_error,而iterator resolve(const query&, boost::system::error_code & ec)修改引用参数ec

当然,对于库来说,好的设计并不是应用程序的好设计:应用程序最好始终使用一种方法。不过,你正在创建一个库,所以如果你愿意,使用boost.asio作为模型可能是一个可行的想法。

  • 使用错误代码,应用程序通常会从那时起继续执行
  • 使用异常,其中应用程序通常从那时起不会继续执行

我实际上不时地混合错误代码和异常。与其他一些答案相反,我不认为这是"丑陋"或糟糕的设计。有时,让函数在出错时抛出异常是不方便的。假设你不在乎它是否失败:

DeleteFile(filename);

有时我不在乎它是否失败(例如,出现"找不到文件"错误)——我只想确保它被删除。这样我就可以忽略返回的错误代码,而不必在它周围放一个尝试捕获

另一方面:

CreateDirectory(path);

如果失败,后面的代码可能也会失败,因此该函数不应该继续。在这里抛出异常很方便。调用方,或者调用堆栈的其他地方,可以弄清楚该怎么做

所以,只要想想如果函数失败,它后面的代码是否有意义。我不认为把两者混为一谈就是世界末日——每个人都知道如何处理两者。

当应该处理错误的代码离检测问题的站点很远(多层)时,异常是很好的。

如果"经常"返回否定状态,并且调用您的代码应该解决该"问题",则状态代码是好的。

自然,这里有一个很大的灰色地带。什么是经常,什么是遥远?

我不建议你同时提供这两种选择,因为这很令人困惑。

我必须承认,我很欣赏您对错误进行分类的方式。

大多数人会说,例外情况应该包括例外情况,但我更喜欢翻译为用户土地:不可恢复。当你知道你的用户无法轻松恢复的事情发生时,然后抛出一个异常,这样他就不必每次打电话给你时都处理它,而是让它冒泡到系统的顶部,记录它。

在剩下的时间里,我倾向于使用嵌入错误的类型。

最简单的是"可选"语法。如果你在集合中寻找对象,那么你可能找不到它。原因只有一个:它不在集合中。因此,错误代码肯定是伪造的。相反,可以使用:

  • 指针(如果您想共享所有权,则共享)
  • 类似指针的对象(类似迭代器)
  • 类似值的可为null的对象(此处赞扬boost::optional<>)

当事情更棘手时,我倾向于使用"替代"类型。实际上,这是haskell中Either类型的概念。要么返回用户要求的内容,要么返回为什么不返回的指示。boost::variant<>和附带的boost::static_visitor<>在这里表现得很好(在函数式编程中,它是通过模式匹配中的对象解构来完成的)。

其主要思想是错误代码可以忽略,但是,如果您返回一个对象,该对象既是函数XOR的结果,又是错误代码,则它不能被静默地丢弃(boost::variant<>boost::optional<>在这里真的很棒)。

我不相信这是一个多么好的想法,但最近我参与了一个项目,他们不能抛出异常,但他们不信任错误代码。因此,它们返回Error<T>,其中T是它们将返回的任何类型的错误代码(通常是某种类型的int,有时是字符串)。如果结果在未经检查的情况下超出范围,并且出现错误,则会引发异常。因此,如果你知道你无能为力,你可以忽略错误并按预期生成异常,但如果你能做点什么,那么你可以显式地检查结果。

这是一个有趣的混合物。

这个问题一直出现在活动列表的顶部,所以我想我会详细介绍一下它是如何工作的。Error<T>类存储了一个类型擦除的异常,因此它的使用不会强制使用特定的异常层次结构或类似的东西,并且每个单独的方法可以随心所欲地抛出任意多个异常。你甚至可以投掷int秒或其他什么;几乎所有带有复制构造函数的东西。

您确实失去了在抛出异常时中断的能力,并最终成为错误的根源。然而,因为实际的异常是最终抛出的[它只是更改的位置],如果您的自定义异常创建了一个堆栈跟踪并在创建时保存它,那么无论何时您有时间捕捉它,该堆栈跟踪都将仍然有效。

一个可能真正破坏游戏规则的大问题是,异常是从其自身的析构函数中抛出的。这意味着您要承担导致应用程序终止的风险。哎呀。

Error<int> result = some_function();
do_something_independant_of_some_function();
if(result)
do_something_else_only_if_some_function_succeeds();

if(result)检查确保错误系统将该错误列为已处理错误,因此result没有理由在销毁时抛出其存储的异常。但如果do_something_independant_of_some_function投掷,result将在到达该检查之前被摧毁。这导致析构函数抛出第二个异常,程序放弃并返回。这非常容易避免[在做任何其他事情之前总是检查函数的结果],但仍然有风险。

这里有一个需要考虑的要点,这是针对锁定机制的,给出关于失败细节的错误代码就像告诉锁选择器锁内4个引脚中的前3个是正确的。

您应该在这里省略尽可能多的信息,并确保您的检查程序总是花费相同的时间来验证卡,这样就不可能发生定时攻击。

但回到最初的问题,一般来说,我更喜欢在所有情况下都提出一个异常,这样调用应用程序就可以通过将调用封装到try/except块中来决定如何处理它们。

我这样做的方法是我有一个Exception类(只是你抛出的异常对象),它由一个字符串消息、一个错误代码枚举和一些漂亮的构造函数组成。

只有当恢复是有意义和可能的时候,我才会把事情包装在try-catch块中。通常,该块将尝试只处理一个或两个枚举的错误代码,同时重新考虑其余的错误代码。在顶层,我的整个应用程序在一个try-catch块中运行,该块记录所有未处理的非致命错误,而如果错误是致命的,则它会退出并显示一个消息框。

你也可以为每种错误有一个单独的Exception类,但我喜欢把它们都放在一个地方。

异常是否完全替换错误代码,或者我可能只需要在"致命情况"中使用它们?

为真正需要的东西保留它们。如果你从一开始就这样写,那么你需要处理/通过的就很少了。把问题放在当地。

混合使用两种范式(异常和错误代码)是个好主意吗?

我认为您的实现应该以错误代码为基础,并在真正异常的情况下使用异常(例如,没有内存,或者您必须捕获抛出的内存)。否则,首选错误代码。

为用户提供两个概念是个好主意吗?

否。并不是每个人都使用例外,也不能保证它们能够安全地跨越边界。将它们公开或放入客户端的域是个坏主意,这会使客户端难以管理您提供的api。他们将不得不处理多个出口(结果代码和异常)有些人将不得不包装你的接口,以便在他们的代码库中使用库。错误代码(或类似的代码)是这里的最低公分母。

您将如何实现这一点?

我不会让客户端出现异常,它更需要维护,并且可以隔离。我会使用错误代码或一个简单的类型,在需要的地方提供额外的字段(例如,如果你想为最终用户提供恢复建议,可以使用字符串字段)。我会尽量把它降到最低。此外,为他们提供了一种测试手段,增加了开发诊断。

为您的逻辑保留库/中间件的本地异常。

请改用错误代码与客户端通信。

异常是否完全替换错误代码,或者我可能只需要在"致命情况"中使用它们?

异常不会普遍替换错误代码。有许多低级函数有几个返回值,根据用户的上下文,这些返回值可能被视为错误,也可能不被视为。例如,许多I/O操作可以返回以下响应:

  • EINTR:操作中断,有时值得重新启动操作,有时表示"用户想退出程序">
  • EWOULDBLOCK:操作是非阻塞的,但现在无法传输任何数据。如果调用方期望可靠的非阻塞行为(例如UNIX域套接字),则这是一个致命错误。如果打电话的人是机会主义地检查,这就是被动的成功
  • 部分成功:在流上下文(例如TCP、磁盘I/O)中,预计会进行部分读/写。在离散消息传递上下文(例如UDP)中,部分读/写可能指示致命的截断

当您在这个级别上工作时,很明显,异常是不合适的,因为实现的作者无法预测哪些"故障响应"将被给定用户视为严重故障,或者只是正常行为。

这在早些时候对我来说真的很困惑,所以为什么我现在还想添加这个问题的答案,因为它肯定是常年存在的:

  • ,您通常应该使用异常而不是错误代码来指示错误。也就是说,默认情况下,如果您将具有错误状态,则应该使用异常。这应该是规则

这个名字就是抛出一个错误的原因——它们是为错误情况而设计的。看看C++STL,它使用异常来指示其错误,如内存不足(std::bad_alloc)等;异常">到正常的执行流程,因为它分支-可能一直回到操作系统(即程序崩溃),而不是;异常";如在";罕见的";。

该规则的例外(heh)是在出现"的情况下;错误";实际上并不是一个错误。错误正确地表示操作未能成功完成,也就是说,它遇到了一些问题,无法产生有效的结果。例如,找不到配置文件,或者找不到用户请求执行操作的文件,表示操作失败。

让人感到困惑的是,有一些东西看起来像";错误";但不是,在这里你通常应该使用一个返回代码——通常是一个";空";无论它通常在哪里给出结果,都会以某种形式发布:我想的是,如果你有一个";查找文件";函数,其特定用途是搜索文件,并返回找到的符合某些条件的文件。如果没有找到任何文件,这就是而不是错误,这只是一个成功完成的搜索,结果为null。

但是,如果你让它搜索一个不存在的目录,或者它失去了网络连接,或者它在构建列表时内存不足,或者。。。那么这就是一个错误。这是它无法成功执行操作的原因。这并不是说它只是没有找到,而是的";看着">您想要执行的过程。它是"出了问题";这就是错误的含义,此类错误应始终作为异常处理。否则,你会一直在脑子里争论该用什么,你不应该这么做。默认情况下选择异常,除非看起来有充分的理由不这样做。