这种使用 C++ 异常是不是合理

Posted

技术标签:

【中文标题】这种使用 C++ 异常是不是合理【英文标题】:Can this use of C++ exceptions justified这种使用 C++ 异常是否合理 【发布时间】:2015-02-14 08:13:22 【问题描述】:

我有一个在错误条件下抛出异常的 C++ API。通常,我在 C++ API 中看到的通知错误的方法是通过特殊的返回代码和函数返回最后一个错误字符串,如果方法返回错误代码,可以检查该字符串。此方法有局限性,例如,如果您需要从函数中返回整数,并且整个整数值范围对返回值有效,因此您无法返回错误代码。

因此,当函数发生错误时,我选择在我的 API 中抛出异常。

这是 C++ 中可接受的异常用法吗?

另外,在我的 API 中的某些函数(例如,authenticate())中,我有两个选择。

    返回 bool 表示成功。 如果失败则返回 void 并抛出异常。

如果使用第一个选项,它与其他函数不一致,因为它们都抛出异常。此外,很难指出错误是什么。

那么在这些函数中也可以使用第二种方法吗?

在下面的回答中,提到使用 C++ 异常来控制程序流是不好的。另外,我在其他地方也听说过。

https://***.com/a/1736162/1015678

我的使用是否违反了这一点?我无法清楚地确定我是否在此处使用异常来控制程序流。

【问题讨论】:

没有明确的是/否答案,这就是为什么链接的答案说“经验法则”。换句话说:程序的正常运行不应该涉及抛出任何异常。 "如果您需要从函数返回一个整数并且整个整数值范围对返回值有效,因此您不能返回错误代码" - 是的,您可以,只是不是方便了。引用或地址的输出参数可以接收这样的结果(错误状态或值,任您选择),您仍然可以保留不是按参数的返回值。但是,这种模式在相对较短的时间内维护和使用确实很烦人。就像这个问题的大部分内容一样,这是一个见仁见智的问题。 回复。在您的第一段中,该函数可以通过引用参数返回状态代码或整数,或者返回tuple。仅仅为了避免添加参考参数而使用异常是不好的设计。 与为错误代码传递引用参数相比,使用异常对我来说看起来更干净,我已经看到它在 java 世界中被大量使用。那么这在 C++ 中是特别糟糕,还是在任何编程语言中通常都很糟糕? 经验法则不是不控制程序流,他们就是这样做的。经验法则是不使用异常来控制正常流,即仅用于错误条件 【参考方案1】:

对于authenticate() 之类的东西,如果您能够为身份验证计算真/假值,我希望您返回一个布尔值,如果有什么阻止您这样做,则抛出异常。关于使用异常进行流控制的评论建议做类似的事情:

try 
   ...
   authenticate();
   // rely on the exception to not execute the rest of the code.
   ...
 catch (...)  ... 

例如,我可以想象一个依赖于联系某个服务的authenticate() 方法,如果您由于某种原因无法与该服务进行通信,那么您不知道凭据是好是坏。

再说一次,API 的另一个主要经验法则是“保持一致”。如果 API 的其余部分依赖异常作为类似情况下的错误值,请使用它,但对我来说,这有点丑陋。我倾向于为例外情况保留例外情况 - 即罕见的,不应该在正常操作期间发生的情况。

【讨论】:

【参考方案2】:

我在 C++ API 中看到的通知错误的方法是通过特殊的返回代码和函数返回最后一个错误字符串,如果方法返回错误代码,可以检查该字符串。

有时这样做是有充分理由的,但更常见的是,当您看到 C++ 库包装了较旧的 C 库、由更熟悉 C 的人编写、为更熟悉 C 的客户端编码人员编写或为互操作性而编写时与 C.

从函数返回一个整数,整个整数值范围对返回值都有效,所以不能返回错误码。

选项包括:

例外情况

以更宽的类型返回(例如,getc() 返回 int-1 表示 EOF)。

在值旁边返回一个成功标志,包裹在boost::optionalpairtuplestruct

至少具有调用者拥有的成功标志和/或值之一,并作为非const 引用或指针参数指定给函数

这是 C++ 中可接受的异常用法吗?

听起来不错,但艺术在于权衡利弊,我们不知道对于调用您的函数的客户端代码是否最方便和健壮。了解他们的关键期望,这将部分基于他们的整体 C++ 经验,但也来自您的 API 的其余部分和与您一起提供的任何其他 API,甚至来自他们可能使用的其他库的其他 API相同的应用程序等。

还要考虑函数的调用者是否可能希望在调用上下文中处理该函数的成功或失败,与其他可能的失败分开。例如,有时客户端代码更容易使用返回布尔成功值的函数:

if (fn(1) && !fn(2))
    fn(3);

try

    fn(1);
    try
    
        fn2();
    
    catch (const ExpectedExceptionFromFn2Type&)
    
        fn3();
    

catch (const PossibleExceptionFromFn1Type&)

    // that's ok - we don't care...

但有时例外情况会更容易:

try

    My_X x  get_X(99) ;
    if (x.is_happy(42))
        x += next_prime_after(x.to_int() * 3);

catch (std::exception& e)

    std::cerr << "oops\n";

...相比...

bool success;
My_X x;
if (get_X(&x, 99)) 
    if (x.is_valid() 
        bool happy;
        if (x.can_get_happy(&happy, 42) && happy) 
            int my_int;
            if (x.can_convert_to_int(&my_int)) 
                if (!x.add(next_prime_after(x.to_int() * 3))) 
                    std::cerr << "blah blah\n";
                    return false;
                 else  cerr / return false; 
             else  cerr / return false; 
         else  cerr / return false; 
     else  cerr / return false; 
 else  cerr / return false; 

(到底有多糟糕取决于函数是否支持报告错误,或者是否可以信任它总是可以工作。这也很困难,因为发生了某些事情使得函数可能开始失败(例如,它开始使用可能丢失的数据文件),如果客户端代码尚未接受并检查错误代码或捕获异常,则一旦识别出潜在的错误,则可能需要重新处理该客户端代码。对于异常而言,情况并非如此,这- 如果你很幸运 - 可能会传播到一些已经合适的客户端 catch 语句,但另一方面,如果至少不关注客户端代码,这是一个冒险的假设。)

一旦您考虑了您所了解的有关客户端使用的一切,可能仍会怀疑哪种方法最好:通常您可以在整个 API 中选择并坚持使用它,但有时您可能希望提供多个版本一个函数,例如:

bool can_authenticate();
void authenticate_or_throw();

...或...

enum Errors_By  Return_Value, Exception ;
bool authenticate(Errors_By e)  ... if (e == Exception) throw ...; return ...; 

...或...

template <class Error_Policy>
struct System

     bool authenticate()  ... Error_Policy::return_or_throw(...); 
     ...

另外,在我的 API 中的某些函数(例如authenticate())中,我有两个选择。

如上所述,您有两个以上的选择。无论如何,一致性非常很重要。听起来例外是合适的。

提到使用 C++ 异常来控制程序流是不好的

这正是异常的作用和它们的全部用途,但我明白你的意思。最终,取得适当的平衡是使用了许多其他软件的艺术,考虑到您的客户将与您一起使用的其他库等。也就是说,如果某事在某种意义上是“错误”,那至少是合理考虑例外情况。

【讨论】:

以上是关于这种使用 C++ 异常是不是合理的主要内容,如果未能解决你的问题,请参考以下文章

C++中,用throw抛出异常后,throw之后的语句是不是继续执行?

用于检查是不是抛出异常的 C++ 断言函数

在 C 回调中在 C++ 中引发异常,可能跨越动态库边界......它安全吗?

有没有办法在 c++ 中使用异常而不会使我增加 60k 的二进制大小?

c++异常处理

您如何判断是不是使异常检查或未检查?