是catch(…){抛出;}`不好的做法?


74

虽然我同意... 不进行重新捕获确实是错误的,但是我相信使用这样的结构:

try
{
  // Stuff
}
catch (...)
{
  // Some cleanup
  throw;
}

在RAII不适用的情况下是可以接受的。(请不要问……不是我公司的每个人都喜欢面向对象的程序设计,RAII通常被视为“无用的学习工具”……)

我的同事说,您应该始终知道要抛出哪些异常,并且始终可以使用类似以下的构造:

try
{
  // Stuff
}
catch (exception_type1&)
{
  // Some cleanup
  throw;
}
catch (exception_type2&)
{
  // Some cleanup
  throw;
}
catch (exception_type3&)
{
  // Some cleanup
  throw;
}

在这些情况下是否存在公认的良好实践?


3
@Pubby:不确定这是完全相同的问题。链接的问题更多是关于“我应该抓住...”的问题,而我的问题集中在“我应该更好地抓住...还是<specific exception>
扔掉

53
抱歉地说,但是没有RAII的C ++不是C ++。
fredoverflow

46
因此,您的奶牛工人不考虑为解决某个特定问题而发明的技术,然后对应该使用哪种劣等替代品进行质疑。抱歉地说,但是不管我用哪种方式看,这似乎都是愚蠢的
2011年

11
“捉住……而不扔掉的确是错误的”-您错了。在中maincatch(...) { return EXIT_FAILURE; }可能很适合未在调试器下运行的代码。如果没有抓住,则可能无法解开堆栈。只有当调试器检测到未捕获的异常时,您才希望它们离开main
史蒂夫·杰索普

3
...因此,即使这是“编程错误”,也不一定会导致您不想了解它。无论如何,您的同事不是优秀的软件专业人员,因此sbi说很难谈论如何最好地处理长期以来一无所获的情况。
史蒂夫·杰索普

Answers:


196

我的同事说,您应该始终知道要抛出哪些异常[...]

我想说的是,您的同事显然从未在通用库上工作过。

像一类的世界怎么能std::vector甚至假装知道是什么的拷贝构造函数将抛出,同时仍然保证异常安全?

如果您始终知道被调用方在编译时会做什么,那么多态将毫无用处!有时整个目标是抽象掉在较低的水平会发生什么,所以你特别想要知道发生了什么事!


32
实际上,即使他们知道要抛出异常。此代码重复的目的是什么?除非处理方式不同,否则我不会列举例外来炫耀您的知识。
Michael Krelin-黑客2011年

3
@ MichaelKrelin-hacker:也是。此外,还要添加一个事实,即它们不赞成使用异常规范,因为在代码中列出所有可能的异常往往会在以后导致错误……这是有史以来最糟糕的想法。
Mehrdad

4
而令我困扰的是,当这种态度与将有用而便捷的技术视为“无用的学校用品”相结合时,可能是这种态度的根源。但好吧……
Michael Krelin-黑客2011年

1
+1,所有可能选项的枚举是未来失败的绝妙秘诀,为什么有人会选择这样做...呢?
littleadv

2
好答案。可能会从提及中受益,如果必须支持的编译器在X区域中存在错误,那么使用X区域中的功能并不明智,至少不要直接使用它。例如,鉴于有关该公司的信息,如果他们使用Visual C ++ 6.0,就不会感到惊讶,Visual C ++ 6.0在该领域中有一些傻虫(例如两次调用异常对象析构函数)-这些早期臭虫的一些较小的后代幸存了下来这一天,但需要精心安排才能体现出来。
Alf P. Steinbach

44

您似乎陷入的困境是某人试图吃蛋糕也要吃蛋糕的特定地狱。

RAII和例外的设计是相辅相成的。RAII是你没有的手段写了很多的catch(...)语句做清理工作。当然,它会自动发生。异常是使用RAII对象的唯一方法,因为构造函数只能成功或抛出(或将对象置于错误状态,但是谁想要呢?)。

catch言可以做两件事情之一:处理错误或特殊情况,或做清理工作。有时它会同时执行这两个操作,但是每个catch语句都存在至少执行其中一个操作的条件。

catch(...)无法执行适当的异常处理。您不知道例外是什么。您无法获取有关异常的信息。除了一个事实是某个代码块内的某个东西引发了异常之外,您绝对没有其他信息。在这种情况下,您唯一可以做的合法事情就是进行清理。这意味着在清理结束时重新抛出异常。

RAII在异常处理方面为您提供的是免费清理。如果一切都被RAII正确封装,那么一切将被正确清理。您不再需要让catch语句进行清理。在这种情况下,没有理由编写catch(...)语句。

因此,我同意这catch(...)主要是邪恶的…… 暂时

该规定是RAII的正确使用。因为没有它,您需要能够进行某些清理。没有解决的办法。您必须能够进行清理工作。您需要确保抛出异常将使代码处于合理状态。并且catch(...)是这样做的重要工具。

你不能没有一个。您不能说RAII catch(...)都不好。您至少需要其中之一;否则,您也不例外。

当然,catch(...)即使RAII都无法消除它的一种有效-尽管很少使用的方法:将其exception_ptr转发给其他人。通常通过一个promise/future或类似的界面。

我的同事说,您应该始终知道要抛出哪些异常,并且始终可以使用类似以下的构造:

您的同事是个白痴(或者非常无知)。由于他建议您编写多少复制和粘贴代码,因此这应该立即显而易见。每个catch语句的清理将完全相同。那是维护的噩梦,更不用说可读性了。

简而言之:这是创建 RAII 来解决的问题(不是说它不能解决其他问题)。

使我感到困惑的是,这通常被大多数人认为RAI​​I不好的观点所落后。通常,该论据是:“ RAII不好,因为您必须使用异常来表示构造函数故障。但是您不能抛出异常,因为它不安全,并且您将不得不使用大量catch语句来清理所有内容。” 这是一个有争议的论点,因为RAII 解决了RAII缺乏造成的问题。

他极有可能反对RAII,因为它隐藏了细节。析构函数调用在自动变量上不立即可见。这样您就可以隐式调用代码。一些程序员真的很讨厌。显然,至于他们认为拥有3条catch语句,所有这些语句都使用复制和粘贴代码执行相同的操作是一个更好的主意。


2
似乎您没有编写提供强大异常安全保证的代码。RAII提供基本保证。但是为了提供有力的保证,您必须撤消一些操作才能将系统还原到调用功能之前的状态。基本保证是清理,有力保证是回滚。回滚是特定于功能的。因此,您不能将其放入“ RAII”中。那就是当所有障碍都变得方便时。如果你写具有很强的保护代码,您使用包罗万象的多。
anton_rh

@anton_rh:也许,但是即使在那种情况下,包罗万象的语句也是最后的手段。首选的工具是更改任何必须在异常情况下恢复的状态之前执行所有抛出的操作。显然,您不能在所有情况下都以这种方式实现所有操作,但这是获得强大的异常保证的理想方法。
Nicol Bolas

14

真的有两个评论。首先,在理想环境中,您应该始终知道可能会抛出哪些异常,实际上,如果您正在使用第三方库或使用Microsoft编译器进行编译,则不会。更重要的是,即使您确实知道所有可能的例外,这里是否也有相关性? 甚至catch (...)catch ( std::exception const& )假设所有可能的异常都来自 std::exception(理想情况下就是这种情况)。至于使用多个捕获块,如果没有所有例外的共同基础:那就是彻底的混淆,以及维护的噩梦。您如何识别所有行为都是相同的?那就是目的吗?如果必须更改行为(例如,错误修复),会发生什么?错过一个太容易了。


3
实际上,我的同事设计了自己的异常类,该异常类并非源自std::exception并且每天尝试在我们的代码库中强制使用它。我的猜测是他试图惩罚我使用他自己没有编写的代码和外部库。
ereOn 2011年

17
@ereOn在我看来,您的同事急需培训。无论如何,我可能会避免使用他编写的库。

2
模板和知道会引发什么异常的事情像花生酱和死壁虎一样一起出现。std::vector<>诸如此类的简单操作几乎可以出于任何原因引发任何异常。
David Thornley

3
请告诉我们,您究竟如何知道明天的错误修复会在调用树中引发什么异常?
mattnz 2012年

11

我认为您的同事提出了一些好的建议-您只应在catch不重新抛出异常的情况下,在一个块中处理已知异常。

这意味着:

try
{
  // Stuff
}
catch (...)
{
  // General stuff
}

不好,因为它会静默地隐藏任何错误。

然而:

try
{
  // Stuff
}
catch (exception_type_we_can_handle&)
{
  // Deal with the known exception
}

很好-我们知道我们要处理的内容,不需要将其公开给调用代码。

同样地:

try
{
  // Stuff
}
catch (...)
{
  // Rollback transactions, log errors, etc
  throw;
}

很好,即使是最佳实践,用于处理一般错误的代码也应该与引起它们的代码一起使用。比依赖被调用方知道事务需要回滚或其他任何操作要好。


9

任何对“ 是”或“ 否”的回答都应附有理由。

说这是错误的,仅仅是因为我已经被教导那样,只是盲目的狂热。

//Some cleanup; throw像您的示例中那样多次编写相同的代码是错误的,因为这是代码重复并且这是维护负担。只写一次更好。

编写一个a catch(...)来使所有异常静音的做法是错误的,因为您应该只处理您知道如何处理的异常,并且使用该通配符,可以超出预期的范围,并且这样做可以使重要错误静音。

但是,如果在a catch(...)之后重新抛出,则后一种原理不再适用,因为您实际上没有在处理异常,因此没有理由不鼓励这样做。

实际上,我已经完成了登录敏感功能的操作,而没有任何问题:

void DoSomethingImportant()
{
    try
    {
        Log("Going to do something important");
        DoIt();
    }
    catch (std::exception &e)
    {
        Log("Error doing something important: %s", e.what());
        throw;
    }
    catch (...)
    {
        Log("Unexpected error doing something important");
        throw;
    }
    Log("Success doing something important");
}

2
希望Log(...)不能扔。
Deduplicator

2

我总体上同意这里的帖子的心情,我真的不喜欢捕获特定异常的模式-我认为它的语法仍处于起步阶段,尚无法解决冗余代码。

但是,由于每个人都这么说,因此我会提出以下事实:即使我很少使用它们,我也经常查看我的“ catch(Exception e)”语句之一,并说:“该死,我希望找出当时的特定例外情况”,因为当您稍后进来时,通常很容易知道意图是什么,客户可能会一目了然。

我并没有说“总是使用x”的态度是正当的,只是说偶尔将它们列出来确实很高兴,而且我敢肯定,这就是为什么有人认为这是“正确”的做法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.