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

ezp*_*sso 25 c++ error-handling exception

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

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

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

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

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

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

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

问题是:

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

更新1.

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

Ser*_*ich 18

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

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

答:他们这样做.对于C++函数,异常总是比返回错误代码更好.原因解释如下.

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

答:不会.混合很难看并且使错误处理不一致.当我被迫使用错误返回API(如Win32 API,POSIX等)时,我使用异常抛出包装器.

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

答:不会.用户会混淆选择哪种变体,并且通常会做出最糟糕的混合决定.有些用户更喜欢其他人更喜欢错误返回的例外情况,如果他们都在同一个项目上工作,他们会使项目的错误处理实践变得一团糟.

问:混合概念的异常和错误代码是否有任何好的例子?

答:没有.如果你找到了,请告诉我.如果必须使用错误返回函数(并且通常必须使用它们),IMO隔离错误返回函数与异常抛出包装器是最佳实践.

问:你会如何实现这个?

答:我只会使用例外.我的方式只是在成功的情况下才回来.错误返回练习大大混淆了带有错误检查分支的代码甚至更糟糕 - 缺少错误状态检查,因此错误状态被忽略,使代码充满了难以揭示的隐藏错误.例外使错误处理孤立.如果你需要就地处理某种错误,通常意味着它根本不是一个错误,而只是一些合法的事件,可以通过指示某些特定状态(通过返回值或其他方式)成功返回来报告.如果你真的需要检查本地是否发生了一些错误(不是来自root try/catch块),你可以在本地尝试/ catch,因此只使用异常并不会以任何方式限制你的能力.

重要的提示:

对于每种特定情况,正确定义什么是错误和什么不是(为了最佳可用性)是非常重要的.

例如,假设我们有一个显示输入对话框并返回用户输入文本的功能,如果用户可以取消输入,则取消事件成功 - 不是错误(但必须以某种方式在返回时指示用户取消输入)但缺少资源(如内存或GDI对象或其他东西)或类似缺少监视器以显示对话框确实是错误.

一般来说:

例外是C++语言更自然的错误处理机制.因此,如果您正在开发仅供C++应用程序使用的C++应用程序或库(而不是C应用程序等),则使用异常是一个好主意.错误返回是更便携的方法 - 您可以将错误代码返回到使用任何编程语言编写的应用程序,甚至可以在不同的计算机上运行.当然,几乎总是OS请求通过错误代码报告其状态(因为很自然地使它们与语言无关).因此,您必须在日常编程中处理错误代码. IMO计划基于错误代码的C++应用程序的错误处理策略只是在寻找麻烦 - 应用程序变得完全无法读取.IMO在C++应用程序中处理状态代码的最佳方法是使用C++包装器函数/类/方法来调用错误返回功能,如果返回错误 - 抛出异常(状态信息嵌入到异常类中).

一些重要的注意事项和警告:

为了在项目中使用异常作为错误处理策略(无论是大型还是小型),编写异常安全代码的严格策略非常重要.它基本上意味着每个资源都是在某个类的构造函数中获取的,更重要的是在析构函数中释放(这将确保您没有资源泄漏).而且你必须在某处捕获异常 - 通常在你的根级函数中(如main或窗口过程或线程过程等).

考虑以下代码:

SomeType* p = new SomeType;

some_list.push_back(p);
/* later element of some_list have to be delete-ed
   after removing them from this list */
Run Code Online (Sandbox Code Playgroud)

这是典型的潜在内存泄漏 - 如果push_back抛出异常然后动态分配和构造SomeType对象泄露.

例外安全变体是这样的:

std::auto_ptr<SomeType> pa( new SomeType );

some_list.push_back(pa.get());
pa.release();
/* later elements of some_list have to be delete-ed
   after removing them from this list */
Run Code Online (Sandbox Code Playgroud)

要么:

boost::shared_ptr<SomeType> pa( new SomeType );

some_list.push_back(pa);
/* some_list is list of boost::shared_ptr<SomeType>
   so everything is delete-ed automatically */
Run Code Online (Sandbox Code Playgroud)

如果您正在使用C++标准模板,分配器等,您可以编写异常安全代码(如果您尝试/捕获每个单独的STL调用代码变得一团糟)或者让代码充满潜在的资源泄漏(不幸的是,这很常见) ).True C++应用程序必须是异常安全的.

  • 我认为对于使用客户端异常进行通信的中间件来说,这是一个糟糕的建议.错误代码虽然不像它们那样笨拙,但侵扰性较小.如果Win32 API使用异常而不是错误代码,则进行映像.它们基本上可以来自许多不同的地方,许多不同的方式和类型.对于内部逻辑,它们是不错的选择,因为您知道抛出什么(记录),以及可以处理什么. (3认同)
  • @malkia :(继续)另一方面,状态代码可能是不同的类型(如`errno`,`GetLastError()`,某些第三方库错误状态代码等),当你从不同的API调用函数并得到您必须决定如何将该API的状态代码转换为您的函数使用的状态代码的错误,这样您通常会丢失特定的错误信息.除了例外,您只需将特定的错误代码(以及所有其他给定的信息)包装到其异常类中,并且不会丢失任何错误信息. (2认同)

Cub*_*bbi 13

混合概念的异常和错误代码是否有任何好的例子?

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

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

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


Ash*_*ain 8

  • 使用错误代码,应用程序通常会从该点继续执行.
  • 在应用程序通常不会从该点继续执行的情况下使用异常.

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

DeleteFile(filename);
Run Code Online (Sandbox Code Playgroud)

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

另一方面:

CreateDirectory(path);
Run Code Online (Sandbox Code Playgroud)

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

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


Bo *_*son 7

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

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

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

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


Mat*_* M. 5

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

大多数人会说异常应该涵盖异常情况,但我更喜欢翻译为用户态:不可恢复。当您知道您的用户无法轻松恢复的事情发生时,然后抛出异常,这样他就不必每次调用您时都处理它,而只是让它冒泡到系统的顶部,在那里它会被记录。

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

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

  • 一个指针(如果你想共享所有权则共享)
  • 类似指针的对象(如迭代器)
  • 类似值的可空对象(boost::optional<>这里值得称赞)

当事情比较棘手时,我倾向于使用“另类”类型。这确实是 haskell 中类型的想法Either。要么返回用户请求的内容,要么返回用户未返回的原因的指示。boost::variant<>和伴随的boost::static_visitor<>游戏在这里很好地发挥(在函数式编程中,它是通过模式匹配中的对象解构来完成的)。

主要思想是错误代码可以被忽略,但是如果您返回一个对象,该对象既是函数异或错误代码的结果,那么它就不能被默默地删除(boost::variant<>并且boost::optional<>在这里真的很棒)。


Den*_*ose 5

我不相信这是一个多么好的主意,但最近我参与了一个项目,他们不能处理异常,但他们不信任错误代码。因此它们返回Error<T>,其中 T 是它们返回的任何类型的错误代码(通常是某种类型的 int,有时是字符串)。如果结果超出范围而没有经过检查,并且出现错误,则会抛出异常。因此,如果您知道无能为力,则可以忽略该错误并按预期生成异常,但如果您可以做某事,则可以显式检查结果。

这是一种有趣的混合物。

这个问题不断出现在活动列表的顶部,所以我想我会稍微扩展一下它是如何工作的。该类Error<T>存储了类型擦除的异常,因此它的使用不会强制使用特定的异常层次结构或类似的东西,并且每个单独的方法都可以根据需要抛出任意数量的异常。你甚至可以抛出ints 或其他什么;几乎所有带有复制构造函数的东西。

您确实失去了在引发异常时中断的能力,并最终找到了错误的根源。但是,因为实际的异常是最终抛出的[它只是更改的位置],所以如果您的自定义异常创建堆栈跟踪并在创建时保存它,则只要您有时间捕获它,该堆栈跟踪仍然有效。

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

Error<int> result = some_function();
do_something_independant_of_some_function();
if(result)
    do_something_else_only_if_some_function_succeeds();
Run Code Online (Sandbox Code Playgroud)

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