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

时间:2011-04-27 14:05:40

标签: c++ exception error-handling

我正在开发一个C ++加密狗通信库。该库将提供一个统一的接口,以便与一系列远程代码执行加密狗(如SenseLock,KEYLOK,Guardant Code)进行通信。

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

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

嗯,所有这些阶段最终都会出错,这是微不足道的。可能有很多种情况,但最常见的是:

  • 找不到加密狗(肯定是一个致命的案例)。
  • 加密狗连接失败(致命案件)。
  • 在加密狗(?)中找不到指定的执行模块。
  • 请求的操作因超时(?)而失败。
  • 请求的操作需要授权(我认为是可恢复的案例)。
  • 在加密狗中执行模块时发生内存错误(肯定是致命的情况)。
  • 加密狗出现文件系统错误(肯定是致命的情况)。

? - 我不知道案件是否被认为是致命的。

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

问题是:

  1. 异常是否完全替换了错误代码,或者我只需要将它们用于“致命案例”?
  2. 混合两种范例(例外和错误代码)被认为是个好主意吗?
  3. 为用户提供两种概念是否是个好主意?
  4. 是否存在混合概念的异常和错误代码的好例子?
  5. 你会如何实现这个?
  6. 更新1。

    从不同角度看更多意见会很有意思,所以我决定在这个问题上增加100点声望。

12 个答案:

答案 0 :(得分:17)

答案 1 :(得分:13)

  

是否存在混合概念的异常和错误代码的好例子?

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

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

当然,什么是图书馆的优秀设计,对于应用程序来说不是一个好的设计:应用程序最好一致地使用一种方法。但是,你正在创建一个库,所以如果你想要它,使用boost.asio作为模型可能是一个可行的想法。

答案 2 :(得分:8)

  • 使用错误代码,此时应用程序通常继续执行
  • 使用例外,此时应用程序通常不会继续执行

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

DeleteFile(filename);

有时候我不在乎它是否失败(例如“找不到文件”错误) - 我只是想确保它被删除。这样我可以忽略返回的错误代码,而不必在它周围放置try-catch。

另一方面:

CreateDirectory(path);

如果失败,则后面的代码可能也会失败,因此该函数不应继续。抛出异常很方便。调用者或调用堆栈的某个位置可以找出要执行的操作。

所以,只要考虑一下,如果函数失败,它后面的代码是否可能会有意义。我不认为混合这两者是世界末日 - 每个人都知道如何处理这两者。

答案 3 :(得分:7)

如果处理错误的代码远离检测到问题的站点(很多层),则异常是好的。

如果预期“经常”返回负状态并且调用您的代码应该处理该“问题”,则状态代码是好的。

当然,这里有一个很大的灰色区域。经常是什么,什么是遥远的?

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

答案 4 :(得分:4)

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

大多数人会说异常应该涵盖特殊情况,我更愿意转换为用户土地:不可恢复。当一些事情发生时你知道你的用户无法轻易恢复,然后抛出异常,这样他每次打电话给你时都不必处理它,但只会让它冒泡到它的系统顶部。记录。

其余的时间,我会使用嵌入错误的类型。

最简单的是“可选”语法。如果您正在寻找集合中的对象,那么您可能找不到它。这里有一个原因:它不在集合中。因此,错误代码肯定是虚假的。相反,人们可以使用:

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

当事情变得棘手时,我倾向于使用“替代”类型。这是haskell中Either类型的想法,真的。要么返回用户要求的内容,要么返回为什么不返回它的指示。 boost::variant<>以及随附的boost::static_visitor<>在这里发挥得很好(在函数式编程中,它通过模式匹配中的对象解构来完成)。

主要思想是错误代码可以被忽略,但是如果你返回一个对象,它既是函数XOR的结果,也是错误代码,那么它就不能被静默删除({{1 }}和boost::variant<>在这里非常棒。)

答案 5 :(得分:4)

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

您应尽可能省略尽可能多的信息,并确保您的检查程序总是花费相同的时间来验证卡,以便无法进行计时攻击。

但是回到最初的问题,一般来说我更喜欢在所有情况下引发异常,因此调用应用程序可以通过将调用包装到try / except块来决定它是如何处理它们的。

答案 6 :(得分:3)

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

当恢复有意义且可能时,我只在try catch块中包装东西。通常,该块将尝试仅处理一个或两个枚举的错误代码,同时重新抛出其余的错误代码。在顶层我的整个应用程序在try catch块中运行,该块记录所有未处理的非致命错误,如果错误是致命的,它将以消息框退出。

对于每种错误,你也可以有一个单独的Exception类,但我喜欢在一个地方拥有它。

答案 7 :(得分:3)

  

异常是否完全替换了错误代码,或者我只需要将它们用于“致命案例”?

保留它们以满足真正的需要。如果你从一开始就这样写,你将需要处理/传递的很少。把问题保持在当地。

  

混合两种范例(例外和错误代码)是否被认为是一个好主意?

我认为你应该根据错误代码来实现你的实现,并在真正特殊的情况下使用异常(例如没有内存,或者你必须抓住一个抛出的内容)。否则,更喜欢错误代码。

  

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

没有。不是每个人都使用例外,并不保证他们安全地跨越边界。暴露它们或将它们扔进客户的域是一个坏主意,并使客户难以管理您提供的api。他们必须处理多个出口(结果代码和异常)有些将必须包装你的接口,以便在他们的代码库中使用库。错误代码(或类似的东西)是这里最低的共同点。

  

你会如何实现这个?

我不会让客户受到例外情况的影响,更多的是维护和隔离。我会使用错误代码或简单类型,在需要时提供其他字段(例如,如果您要为最终用户提供恢复建议,则为字符串字段)。我会尽量保持最低限度。此外,为他们提供了一种手段,可以通过增加诊断开发进行测试。

答案 8 :(得分:3)

我并不知道这是多么好的想法,但最近我在一个他们不能抛弃异常的项目上工作,但他们不相信错误代码。所以他们返回Error<T>,其中T是他们将返回的任何类型的错误代码(通常是某种类型的int,有时是字符串)。如果结果超出范围而未进行检查,并且出现错误,则会抛出异常。因此,如果您知道无法执行任何操作,则可以忽略错误并按预期生成异常,但如果您可以执行某些操作,则可以显式检查结果。

这是一个有趣的混合物。

这个问题不断弹出活跃列表的顶部,所以我想我会扩展一下这是如何工作的。 Error<T>类存储了一个类型擦除的异常,因此它的使用并没有强制使用特定的异常层次结构或类似的东西,并且每个单独的方法都可以抛出许多例外情况。你甚至可以抛出int或其他什么;几乎所有与复制构造函数的东西。

你确实失去了抛出异常抛出的能力,最终导致了错误的来源。但是,因为实际异常是最终抛出的[它只是更改的位置],如果您的自定义异常创建堆栈跟踪并在创建时保存它,那么只要您开始捕获它,该堆栈跟踪仍然有效。 / p>

可能是一个真正的游戏破坏者的一个大问题是异常是从它自己的析构函数中抛出的。这意味着您冒着风险导致应用程序终止。糟糕。

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将在到达该检查之前被销毁。这导致析构函数抛出第二个异常,程序只是放弃并回家。这很容易避免[在做其他任何事情之前总是检查一个函数的结果],但仍然存在风险。

答案 9 :(得分:2)

  

异常是否完全替换了错误代码,或者我只需要将它们用于“致命案例”?

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

  • EINTR:操作中断,有时值得重启操作,有时则意味着“用户想退出程序”
  • EWOULDBLOCK:操作是非阻塞的,但现在无法传输数据。如果调用者期望可靠的非阻塞行为(例如UNIX域套接字),则这是致命错误。如果呼叫者在机会上进行检查,则这是被动的成功。
  • 部分成功:在流式上下文(例如TCP,磁盘I / O)中,期望部分读/写。在离散消息传递上下文(例如UDP)中,部分读/写可能表示致命截断。

当你在这个级别工作时,很明显异常是不合适的,因为实现的作者无法预测哪些“失败响应”将实际视为给定用户的严重失败,或者只是正常的行为。

答案 10 :(得分:2)

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

使用错误代码代替与客户沟通。

答案 11 :(得分:0)

在前几天,这对我来说真的也是令人困惑的地狱,所以为什么我现在还是想为这个问题添加答案,因为它肯定是多年生的:

  • ,通常应该使用例外而不是错误代码来表示错误。也就是说,默认情况下,如果将要处于错误状态,则应使用异常。那应该是规则。

这个名字是一掷千金-它们是为错误情况设计的。看一下C ++ STL,它使用异常来指示其错误,例如内存不足(std::bad_alloc)等。我相信,实际名称是要向其指示“异常”的概念。正常的执行流程,因为它分支了出去-可能一直回到操作系统(即程序崩溃),而不是“稀有”中的“例外”。

此规则的例外情况(heh)是似乎“错误”实际上不是错误。错误 正确地表示操作未能成功完成,也就是说,它遇到了导致其无法产生有效结果的问题。例如,未找到配置文件或未找到用户请求执行操作的文件,则表示操作失败。

让人感到困惑的地方是,有些东西看起来像“错误”,但不是,在这里您通常应该使用返回码-通常是某种形式的“空”它通常会在通常会产生结果的地方显示出来:我想到的是,如果您有一个“查找文件”功能,其特定目的用于搜索文件,然后返回它找到了符合某些条件的文件。如果没有找到文件,这不是 错误,那只是一个成功完成的搜索,显示为空。

但是,例如,如果您要求它搜索不存在的目录,或者它失去了网络连接,或者它在构建列表时用完了内存,或者...那么就是一个错误。这是使它无法成功执行操作的原因。并非只是找不到原因,而是您想要执行的“外观” 过程出现了错误错误的意思是“出事了”,应始终将其视为异常。否则,您将不断在争论使用什么,而不应该这样做。除非看起来有充分的理由不这样做,否则默认情况下请选择例外。