在抛出异常时构造对象时抛出异常?

3

我有一个异常类:

class MyException : public std::exception
{
public:
    MyException( char* message )
        : message_( message )
    {
        if( !message_ ) throw std::invalid_argument("message param must not be null");
    }
};

在我的投掷位置:
try {
    throw MyException( NULL );
}
catch( std::exception const& e ) {
    std::cout << e.what();
}

(因为代码未编译,所以请忽略任何错误)

我想知道在构造函数中由于另一个抛出而导致自己抛出时会发生什么。我认为这是合法的,捕获将会捕获到 std::invalid_argument,之前抛出的异常 (MyException) 将被忽略或取消。

我设计的目的是在我的异常类中强制实施不变量。 message_ 永远不应该为 NULL,并且我不想在我的 what() 重载中检查它是否为 null,所以我在构造函数中检查它们是否无效,如果无效就抛出异常。

这是正确的吗?还是行为有所不同?


只要你在某个地方捕获它...你考虑使用const std::string& message,这样就不是问题了吗? - AJG85
如果你从 std::runtime_error 派生,你不需要自己存储消息。将消息传递给 std::runtime_error 的构造函数即可。注意:一些流行但错误的实现允许你将消息传递给 std::exception,不幸的是这不符合标准,并且在移植时会导致问题。 - Martin York
@LokiAstari 我不使用 std::runtime_error(尽管我很想使用),因为它要求在构造期间形成完整的 what() 字符串。我喜欢将该字符串处理推迟到调用 what() 时,因此我使用 std::exception 来实现这个目的。 - void.pointer
@RobertDailey:我不明白你在说什么。std::exception不支持字符串的惰性求值。另外,我认为那是一种虚假的优化。 - Martin York
@LokiAstari,您误解了。我只是推迟执行我的字符串构建逻辑,直到调用what()函数。std::runtime_error在内部存储一个字符串,并且需要在其构造函数中使用一个字符串。这几乎强制您在构造异常对象时为what()构建字符串。明白了吗? - void.pointer
显示剩余4条评论
3个回答

4
你打算抛出的对象(在这种情况下是MyException)必须在抛出之前成功构造。因此,你现在还没有抛出它,因为它还没有被构造。所以,在MyException的构造函数中抛出异常将起作用。你不会触发“在处理异常时抛出异常会导致std::terminate”问题。

我试图在标准中找到这个,但是我没有看到任何相关内容。这个特定情况是否在2003年的标准中提到? - void.pointer
2
@RobertDailey:不会啊,为什么会呢?在throw执行之前,它必须首先成功地评估其表达式。而且在throw执行之前,还没有任何异常被抛出。由于该表达式未能成功评估,因此throw从未发生过。这是C++许多独立部分的必然结果。 - Nicol Bolas
2
@NicolBolas:你的假设是正确的,但我不认同这个论点。有人可以争辩(虽然我不是)抛出已经在任何部分的“throw语句”开始时就已经被初始化了(throw语句包括抛出表达式),即在您跨越上一个语句的“序列点”之后。幸运的是,标准对此问题非常明确,请参见:15.1.7(在n3376中)。 - Martin York

3

15.1 抛出异常 n3376

第7段

如果异常处理机制在完成要抛出的表达式的评估之后但在捕获异常之前调用退出函数,则会调用 std::terminate(15.5.1)。

这意味着,在构造函数(在本例中被抛出的对象的构造函数)完成之前,不会发生任何特殊的情况。但是,在构造函数完成后,任何其他未被捕获的异常都将导致调用 terminate()

标准进一步提供了一个示例:

struct C
{
       C() { }
       C(const C&) { throw 0; }
};

int main()
{
  try
  {
    throw C();   // calls std::terminate()
  }
  catch(C) { }
}

因为对象首先被创建,所以调用了terminate。但是随后调用复制构造函数将异常复制到持有位置(15.1.4)。在此函数调用(复制构造)期间,生成未捕获的异常,因此调用terminate。

因此,按照所示代码应该按预期工作。

  • 要么:生成一个带有良好消息并抛出的MyException
  • 要么:生成并抛出一个std::invalid_argument

0

如果您想检查应始终为真的不变量,您应该使用断言。异常是用于预期会发生的不寻常情况,例如边角情况或错误的用户输入。

如果您使用异常来报告代码中的错误,您可能会意外地使用catch(...)隐藏它们。如果您编写库代码,这一点尤其重要,因为您永远不知道其他人是否会捕获并忽略异常,即使这意味着系统已达到无效状态。

断言的另一个优点是,如果您希望,可以完全禁用它们,这样它们将不再产生任何性能损失。这允许您在调试版本中对这些检查进行非常谨慎的检查,并仍然拥有闪电般快速的发布版本。


断言的缺点是它们通常在运行时被禁用。有时,即使它是应该始终为真的东西,你仍然希望在运行时检查它,甚至在发布模式下也是如此。 - David Stone
如果程序员想用catch(...)来隐藏所有异常,那就是糟糕的编程实践。这就好像反对assert一样,因为通常它被实现为调用exit(或某个变体),我可以注册一个函数在退出时被调用,然后跳回到正常执行并以此方式隐藏错误。你无法防御一个有权访问代码并与你作对的程序员。 - David Stone
你不必使用标准的assert()宏,你也可以定义自己的宏。这样,你就可以有一些在发布版本中应该禁用的断言和其他应该保留的断言。如果你抛出一个异常,那么它应该是接口的一部分,并且调用你的函数或构造函数的人必须考虑如何处理它。OP描述的情况除了告诉用户程序存在错误并崩溃之外,没有任何合理的处理方式。如果没有合理的处理方法,那么它就不应该成为异常。 - Benjamin Schug
总有合理的处理方式。我有一个程序,可以搜索游戏树到一定深度。有时,会进入无法恢复的无效状态。但是,我是在真实数据的副本上进行搜索的(自然涉及大量复制)。如果我进入这种状态,我可以使用断言(状态始终有效!)或者抛出异常。我选择抛出异常,因为我的处理方式只是报告错误并返回深度-1的搜索结果。我将深度=0定义为“随机结果”。 - David Stone
同样的程序登录服务器来玩游戏。我可以使用一个“始终在”的断言,以便在出现诸如“套接字意外关闭”或“服务器发送无效数据”之类的情况下终止程序。最后一个意味着我不能信任我的连接是有效的,并且我没有办法知道一条消息从哪里开始和结束。我使用异常,因为我的行为是关闭套接字,等待10秒钟并重新连接到服务器。不处理异常就像调用终止程序一样,但您可以给用户处理该异常的选项。我不想决定错误必须总是致命的。 - David Stone
你在这里描述的是非常不同的东西。在你的情况下,这些事情最终会发生,你的程序应该能够优雅地处理它们。这些是函数调用者应该考虑的事情。OP描述了一种情况,即某人(他自己或库用户)以不正确的方式使用他的API,也就是说,除非代码中存在严重的错误,否则不可能到达这种情况。这是断言的情况。 - Benjamin Schug

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