我应该在 C++ 中使用异常说明符吗?

Posted

技术标签:

【中文标题】我应该在 C++ 中使用异常说明符吗?【英文标题】:Should I use an exception specifier in C++? 【发布时间】:2010-09-10 10:55:30 【问题描述】:

在 C++ 中,您可以通过使用异常说明符来指定函数可能会或可能不会抛出异常。例如:

void foo() throw(); // guaranteed not to throw an exception
void bar() throw(int); // may throw an exception of type int
void baz() throw(...); // may throw an exception of some unspecified type

由于以下原因,我对实际使用它们表示怀疑:

    编译器并没有真正以任何严格的方式强制执行异常说明符,因此好处不是很大。理想情况下,您希望得到一个编译错误。 如果函数违反异常说明符,我认为标准行为是终止程序。 在 VS.Net 中,将 throw(X) 视为 throw(...),因此对标准的遵守不强。

您认为应该使用异常说明符吗? 请回答“是”或“否”,并提供一些理由来证明您的回答。

【问题讨论】:

"throw(...)" 不是标准的 C++。我相信它是某些编译器中的扩展,通常与无异常规范具有相同的含义。 【参考方案1】:

没有。

以下是几个例子:

    模板代码不可能用异常规范编写,

    template<class T>
    void f( T k )
    
         T x( k );
         x.x();
    
    

    副本可能会抛出,参数传递可能会抛出,x() 可能会抛出一些未知异常。

    异常规范倾向于禁止可扩展性。

    virtual void open() throw( FileNotFound );
    

    可能演变成

    virtual void open() throw( FileNotFound, SocketNotReady, InterprocessObjectNotImplemented, HardwareUnresponsive );
    

    你真的可以这样写

    throw( ... )
    

    第一个不可扩展,第二个过于雄心勃勃,第三个才是真正的意思,当您编写虚函数时。

    遗留代码

    当您编写依赖于另一个库的代码时,您真的不知道当出现可怕的错误时它会做什么。

    int lib_f();
    
    void g() throw( k_too_small_exception )
     
       int k = lib_f();
       if( k < 0 ) throw k_too_small_exception();
    
    

    g 将在lib_f() 抛出时终止。这(在大多数情况下)不是您真正想要的。 std::terminate() 永远不应该被调用。让应用程序因未处理的异常而崩溃(您可以从中检索堆栈跟踪)总是比静默/暴力死亡要好。

    编写返回常见错误并在异常情况下抛出的代码。

    Error e = open( "bla.txt" );
    if( e == FileNotFound )
        MessageUser( "File bla.txt not found" );
    if( e == AccessDenied )
        MessageUser( "Failed to open bla.txt, because we don't have read rights ..." );
    if( e != Success )
        MessageUser( "Failed due to some other error, error code = " + itoa( e ) );
    
    try
    
       std::vector<TObj> k( 1000 );
       // ...
    
    catch( const bad_alloc& b )
     
       MessageUser( "out of memory, exiting process" );
       throw;
    
    

尽管如此,当您的库只是抛出您自己的异常时,您可以使用异常规范来说明您的意图。

【讨论】:

在 3 中,从技术上讲,它将是 std::unexpected 而不是 std::terminate。但是当这个函数被调用时,默认调用 abort()。这会生成核心转储。这比未处理的异常更糟糕吗? (基本上做同样的事情) @Greg Rogers:未捕获的异常仍然会展开堆栈。这意味着将调用析构函数。在这些析构函数中,可以做很多事情,比如:正确释放资源、正确写入日志、告知其他进程当前进程正在崩溃等。总而言之,就是 RAII。 你遗漏了:这有效地将所有内容包装在 try ... catch (&lt;specified exceptions&gt;) &lt;do whatever&gt; catch (...) unexpected(); ] 构造中,无论你是否想要一个 try 块。 @paercebal 这是不正确的。是否为未捕获的异常运行析构函数是由实现定义的。如果没有捕获到异常,大多数环境不会展开堆栈/运行析构函数。如果你想确保你的析构函数即使在抛出异常但未被处理(这是可疑的价值)时也能运行,你需要编写像try &lt;&lt;...code...&gt;&gt; catch(...) /* stack guaranteed to be unwound here and dtors run */ throw; /* pass it on to the runtime */ 这样的代码 "让应用程序因未处理的异常而崩溃(您可以从中检索堆栈跟踪)总是比静默/暴力死亡要好。" “崩溃”比对terminate() 的干净调用更好?为什么不直接打电话给abort()【参考方案2】:

避免 C++ 中的异常规范。您在问题中给出的原因是一个很好的开始。

请参阅 Herb Sutter 的 "A Pragmatic Look at Exception Specifications"。

【讨论】:

@awoodland:在 C++11 中不推荐使用“动态异常规范”(throw(optional-type-id-list))。它们仍在标准中,但我想已经发出警告,应该仔细考虑它们的使用。 C++11 添加了noexcept 规范和运算符。我对noexcept 的了解不够多,无法对此发表评论。这篇文章似乎相当详细:akrzemi1.wordpress.com/2011/06/10/using-noexceptDietmar Kühl 在 2011 年 6 月的 Overload Journal 中有一篇文章:accu.org/var/uploads/journals/overload103.pdf @MichaelBurr 只有throw(something) 被认为是无用的并且是个坏主意。 throw() 很有用。 "这是许多人认为异常规范所做的: bullet 保证函数只会抛出列出的异常(可能没有)。 none) 将被抛出。同样,上述预期看似接近正确" 不,上述预期绝对正确。【参考方案3】:

我认为标准的例外约定(对于 C++) 异常说明符是 C++ 标准中的一个实验,但大部分都失败了。 例外是 no throw 说明符很有用,但您还应该在内部添加适当的 try catch 块以确保代码与说明符匹配。 Herb Sutter 有一个关于这个主题的页面。 Gotch 82

此外,我认为值得描述异常保证。

这些基本上是关于对象的状态如何受到该对象上的方法转义的异常影响的文档。不幸的是,编译器并未强制执行或以其他方式提及它们。Boost and Exceptions

异常保证

无保证:

无法保证异常转义方法后对象的状态 在这些情况下,不应再使用该对象。

基本保证:

在几乎所有情况下,这应该是方法提供的最低保证。 这保证了对象的状态定义良好并且仍然可以始终如一地使用。

强力保证:(又名交易保证)

这保证该方法将成功完成 否则会抛出异常,对象状态不会改变。

无投掷保证:

该方法保证不允许任何异常传播到该方法之外。 所有的析构函数都应该做出这个保证。 |注:如果异常在传播过程中从析构函数中转义 |应用程序将终止

【讨论】:

保证是每个 C++ 程序员都必须知道的,但在我看来,它们与异常规范无关。 @David Thornley:我认为保证应该是异常规范(即,具有强 G 的方法不能在没有保护的情况下调用具有基本 G 的方法)。不幸的是,我不确定它们的定义是否足够好,是否可以以一种有用的方式被编译器强制执行。 "这是许多人认为异常规范所做的: - 保证函数只会抛出列出的异常(可能没有)。 - 根据只列出异常的知识启用编译器优化(可能none) 将被抛出。再次,上述期望看似接近正确。" 实际上,两者都完全正确。 @curiousguy。 Java 就是这样做的,因为它检查是在编译时强制执行的。 C++ 是运行时检查。所以:Guarantee that functions will only throw listed exceptions (possibly none)。不对。它只保证如果函数确实抛出这些异常,应用程序将终止。 Enable compiler optimizations based on the knowledge that only listed exceptions (possibly none) will be thrown 不能那样做。因为我刚刚指出你不能保证不会抛出异常。 @LokiAstari “Java 就是这样做的,因为它在编译时执行检查_”Java 异常规范是一个失败的实验。 Java 没有最有用的异常规范:throw()(不抛出)。 “它只保证如果函数确实抛出这些异常,应用程序将终止” 没有“不正确”意味着正确。 C++ 中不能保证函数永远不会调用terminate()。 “因为我刚刚指出你不能保证不会抛出异常” 你保证函数不会抛出。这正是您所需要的。【参考方案4】:

当您违反异常规范时,gcc 会发出警告。我所做的是使用宏仅在“lint”模式下使用异常规范,明确编译以检查以确保异常与我的文档一致。

【讨论】:

【参考方案5】:

唯一有用的异常说明符是“throw()”,如“不抛出”。

【讨论】:

能否请您添加一个有用的原因? 为什么没用?没有什么比知道某个函数不会开始左右抛出异常更有用的了。 有关详细解释,请参阅 Michael Burr 的回答中引用的 Herb Sutter 讨论。【参考方案6】:

异常规范在 C++ 中并不是非常有用的工具。但是,如果与 std::unexpected 结合使用,它们 /is/ 会有很好的用途。

我在一些项目中所做的是带有异常规范的代码,然后调用 set_unexpected() 函数,该函数会抛出我自己设计的特殊异常。此异常在构造时获得回溯(以特定于平台的方式)并从 std::bad_exception 派生(以允许在需要时传播它)。如果它像通常那样导致终止()调用,则回溯由 what() 打印(以及导致它的原始异常;不难找到),所以我得到了我的合同在哪里的信息违反了,比如抛出了什么意外的库异常。

如果我这样做,我绝不允许传播库异常(std 异常除外)并从 std::exception 派生我的所有异常。如果库决定抛出,我将捕获并转换为我自己的层次结构,让我始终控制代码。出于显而易见的原因,调用依赖函数的模板函数应该避免异常规范;但无论如何,很少有带有库代码的模板化函数接口(很少有库真正以有用的方式使用模板)。

【讨论】:

【参考方案7】:

如果您编写的代码将被那些宁愿查看函数声明而不是其周围的任何 cmets 的人使用,那么规范将告诉他们他们可能想要捕获哪些异常。

否则,我发现除了使用 throw() 来表示它不会引发任何异常之外,我发现它并没有特别有用。

【讨论】:

【参考方案8】:

没有。如果您使用它们并引发了您未指定的异常,无论是由您的代码还是由您的代码调用的代码,那么默认行为是立即终止您的程序。

另外,我相信它们在当前的 C++0x 标准草案中已被弃用。

【讨论】:

【参考方案9】:

“throw()”规范允许编译器在执行代码流分析时执行一些优化,前提是它知道函数永远不会抛出异常(或至少承诺永远不会抛出异常)。 Larry Osterman 在这里简要介绍了这一点:

http://blogs.msdn.com/larryosterman/archive/2006/03/22/558390.aspx

【讨论】:

【参考方案10】:

通常我不会使用异常说明符。但是,如果有任何其他异常来自所讨论的函数,而程序肯定无法更正,那么它可能会很有用。在所有情况下,请务必清楚地记录该函数可能会出现哪些异常。

是的,从带有异常说明符的函数抛出的未指定异常的预期行为是调用 terminate()。

我还将注意到 Scott Meyers 在更有效的 C++ 中解决了这个主题。他的 Effective C++ 和 More Effective C++ 是强烈推荐的书籍。

【讨论】:

【参考方案11】:

是的,如果您关注内部文档。或者也许编写一个其他人将使用的库,这样他们就可以在不查阅文档的情况下知道发生了什么。投掷或不投掷都可以视为 API 的一部分,几乎就像返回值一样。

我同意,它们对于在编译器中强制 Java 样式的正确性并没有真正有用,但总比没有或随意的 cmets 好。

【讨论】:

【参考方案12】:

它们对于单元测试很有用,因此在编写测试时,您可以知道函数失败时会抛出什么,但在编译器中没有围绕它们的强制措施。我认为它们是 C++ 中不需要的额外代码。无论您选择哪一个,您都应该确保在整个项目和团队成员中遵循相同的编码标准,以便您的代码保持可读性。

【讨论】:

【参考方案13】:

来自文章:

http://www.boost.org/community/exception_safety.html

“众所周知,不可能 编写一个异常安全的泛型 容器。”经常听到这种说法 参考汤姆的一篇文章 Cargill [4] 在其中探索了 异常安全问题 通用堆栈模板。在他的 文章,嘉吉提出了许多有用的 问题,但不幸的是未能 提出解决他的问题的方法。1 他 最后建议一个 可能无法解决。 不幸的是,他的文章被 许多人作为这种猜测的“证据”。 自发布以来,已经有 许多异常安全的例子 通用组件,其中包括 C++ 标准库容器。

确实,我可以想办法让模板类异常安全。除非您无法控制所有子类,否则无论如何您都可能遇到问题。为此,可以在您的类中创建定义各种模板类抛出的异常的 typedef。这认为问题总是在事后解决,而不是从一开始就设计它,我认为真正的障碍是这种开销。

【讨论】:

【参考方案14】:

异常规范 = 垃圾,询问任何 30 岁以上的 Java 开发人员

【讨论】:

30岁以上的java程序员应该会因为不会C而感到难过,他们没有理由使用java。

以上是关于我应该在 C++ 中使用异常说明符吗?的主要内容,如果未能解决你的问题,请参考以下文章

C++ Primer 5th笔记(chap 18 大型程序工具)noexcept

华为OD机试模拟题用 C++ 实现 - 异常的打卡记录(2023.Q1)

除非另有说明,否则 Bootstrap 中的 col 高度不应该只覆盖内容吗?

捕获多个自定义异常? - C++

老男孩教育每日一题-2017年3月20日:使用rsync守护进程模式,经常会出现的异常情况说明,并说明应该如何解决

C++ 枚举类型声明可以放在 int main 的前面吗?