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

136

在 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

我对实际使用它们持怀疑态度,原因如下:

  1. 编译器在任何严格的方式下都不能很好地强制执行异常规范,所以其益处并不大。理想情况下,您希望得到一个编译错误。
  2. 如果函数违反了异常规范,我认为标准行为是终止程序。
  3. 在VS.Net中,它将throw(X)视为throw(...),所以对标准的遵守程度不高。

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


7
"throw(...)" 不是标准的 C++ 语法。我相信它是一些编译器中的扩展,通常与不指定异常规格具有相同的意义。 - Richard Corden
14个回答

107

不行。

以下是几个例子:

  1. Template code is impossible to write with exception specifications,

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

    The copies might throw, the parameter passing might throw, and x() might throw some unknown exception.

  2. Exception-specifications tend to prohibit extensibility.

    virtual void open() throw( FileNotFound );
    

    might evolve into

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

    You could really write that as

    throw( ... )
    

    The first is not extensible, the second is overambitious and the third is really what you mean, when you write virtual functions.

  3. Legacy code

    When you write code which relies on another library, you don't really know what it might do when something goes horribly wrong.

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

    g will terminate, when lib_f() throws. This is (in most cases) not what you really want. std::terminate() should never be called. It is always better to let the application crash with an unhandled exception, from which you can retrieve a stack-trace, than to silently/violently die.

  4. Write code that returns common errors and throws on exceptional occasions.

    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;
    }
    

然而,当你的库只抛出自己的异常时,你可以使用异常规范来说明你的意图。


1
在 C++ 中,实际上应该是使用 std::unexpected 而不是 std::terminate。但是当调用此函数时,默认情况下会调用 abort()。这将生成一个核心转储。这与未处理的异常有何区别?(两者基本上都会做相同的事情) - Greg Rogers
6
未捕获的异常仍会进行堆栈展开。这意味着析构函数将被调用。在这些析构函数中,可以执行许多操作,例如:正确释放资源,正确写入日志,告知其他进程当前进程正在崩溃等等。总之,这是RAII。 - paercebal
你忘了这一点:这有效地将所有内容包装在try {...} catch (<指定的异常>) { <做任何事情> } catch (...) { unexpected(); ]结构中,无论你是否想要在那里使用try块。 - David Thornley
5
@paercebal,那是不正确的。对于未捕获异常是否运行析构函数是实现定义的。大多数环境在异常未被捕获时不会解开堆栈或运行析构函数。如果您希望确保在抛出未处理的异常时析构函数可移植地运行(这具有可疑的价值),则需要像这样编写代码:try { <<...code...>> } catch(...) /* stack guaranteed to be unwound here and dtors run */ { throw; /* pass it on to the runtime */ } - Logan Capaldo
1
“让应用程序因为未处理的异常而崩溃总是比默默或强制退出更好,这样可以检索调用堆栈。为什么“崩溃”比干净的terminate()调用更好?为什么不直接调用abort()?” - curiousguy

41

避免在C++中使用异常规格说明(exception specifications)。你在问题中提到的原因已经是一个相当好的开端。

请参考Herb Sutter的“实用视角看异常规格说明”


3
“dynamic-exception-specifications” (throw(optional-type-id-list)) 在 C++11 中已经被弃用。它们仍然在标准中存在,但我想警告已经发出,使用它们应该谨慎考虑。C++11 增加了 noexcept 的规范和运算符。我对 noexcept 的细节了解不够,无法发表评论。这篇文章似乎非常详细:http://akrzemi1.wordpress.com/2011/06/10/using-noexcept/ ,Dietmar Kühl 在 2011 年 6 月的 Overload Journal 上也有一篇文章:http://accu.org/var/uploads/journals/overload103.pdf - Michael Burr
@MichaelBurr 只有 throw(something) 被认为是无用和不好的想法。throw() 是有用的。 - curiousguy
以下是许多人对异常规范的理解:• 保证函数只会抛出列出的异常(可能没有)。 • 基于只会抛出列出的异常(可能没有)的知识,启用编译器优化。 上述期望再次具有欺骗性地接近正确。不,上述期望是绝对正确的。 - curiousguy

14

我认为标准例外规范(C++)通常是一个失败的实验。

唯一的例外是no throw规范是有用的,但你也应该在内部添加适当的try catch块,以确保代码匹配规范。Herb Sutter在这个主题上有一页。 Gotch 82

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

它们基本上是关于对象状态如何受到方法中异常影响的文档说明。不幸的是,编译器没有强制执行或提到它们。
Boost和异常

异常保证

无保证:

方法中的异常逃逸后,对象的状态没有任何保证。
在这些情况下,不应再使用该对象。

基本保证:

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

强保证:(又称事务性保证)

这保证了方法将成功完成
或会抛出异常,但对象状态不会改变。

无抛出保证:

该方法保证不允许任何异常从该方法中传播。
所有的析构函数都应该提供此保证。
| 注意:如果在异常已经传播的情况下,析构函数抛出异常,应用程序将终止


保证是每个C++程序员必须了解的内容,但它们似乎与异常规范无关。 - David Thornley
1
@David Thornley:我认为,保证是异常规范应该具有的特性(即,带强烈保证的方法不能在没有保护的情况下调用基本保证的方法)。不幸的是,我不确定它们是否被定义得足够好,可以被编译器以有用的方式执行。 - Martin York
1
@LokiAstari:“这就是Java的做法,因为它在编译时执行检查。” Java异常规范是一个失败的实验。Java没有最有用的异常规范:throw()(不抛出)。“它只保证如果函数确实抛出这些异常,应用程序将终止。” “没有‘不真实’的意思就是真的。在C++中,不能保证函数永远不会调用terminate()。” “因为我刚刚指出你不能保证异常不会被抛出。” “你保证函数不会抛出异常。这正是你所需要的。” - curiousguy
潜在异常 每个函数f、函数指针fp和成员函数指针mfp都有一组潜在异常,其中包括可能被抛出的类型。所有类型的集合表示可能抛出任何异常。该集合定义如下:1)如果f、fp或mfp的声明使用throw()(已弃用)或noexcept,则该集合为空。 - curiousguy
@curiousguy:N3690已经过时将近两年了:当前版本的标准是:N4618 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4618.pdf 或 https://github.com/cplusplus/draft/blob/master/papers/n4618.pdf 这里是标准版本的列表:https://dev59.com/wnVD5IYBdhLWcg3wHnyd#4653479 - Martin York
显示剩余7条评论

10

当违反异常规范时,gcc 会发出警告。我的做法是使用宏仅在特定编译模式下使用异常规范,以检查异常是否符合我的文档说明。


7
唯一有用的异常说明符是 "throw()",即 "不抛出异常"。

2
你能否加上一个说明为什么它有用呢? - buti-oxa
2
为什么这不是有用的呢?没有什么比知道某个函数不会一直抛出异常更有用了。 - Matt Joiner
1
请参阅Michael Burr答案中引用的Herb Sutter讨论,以获取详细说明。 - Harold Ekstrom

4

在C++中,异常规范并不是非常有用的工具。然而,如果与std::unexpected结合使用,仍然有一个很好的用途。

我在一些项目中使用异常规范编写代码,然后调用set_unexpected()函数,使用我的自定义特殊异常抛出函数作为参数。这个异常在构造时以平台特定的方式获取backtrace,并派生自std::bad_exception(允许它在需要时传播)。如果它导致了terminate()调用(通常是这样),那么what()将打印backtrace(以及原始异常引起的信息;找到它不太困难),因此可以得到违反契约的信息,例如抛出了哪个意外的库异常。

如果我这样做,就不允许传播库异常(除std之外的所有异常)并从std::exception派生所有异常。如果库决定抛出异常,我会捕获并转换成自己的层次结构,始终让我控制代码。对于明显的理由,调用依赖函数的模板函数应避免使用异常规范;但是与库代码接口的模板函数非常少见(很少有库以有用的方式使用模板)。


3

3

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

否则,我认为只使用throw()来表示它不会抛出任何异常是没有什么特别有用的。


3

是的,如果你需要内部文档,或者写一个他人会使用的库,这样他们就不必查阅文档就能知道发生了什么。抛出异常与否可以被视为API的一部分,几乎像返回值。

我同意,它们并不是用于在编译器中强制执行Java风格的正确性,但这总比没有或随意的注释好。


3

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接