【问题标题】:Is `catch(...) { throw; }` a bad practice?是 `catch(...) { throw; }` 一个不好的做法?
【发布时间】:2011-12-05 09:05:07
【问题描述】:

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

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

对于这些情况,是否有公认的良好做法?

【问题讨论】:

  • @Pubby:不确定这是不是完全相同的问题。链接的问题更多关于“我应该抓住...”,而我的问题集中在“我应该在重新抛出之前更好地抓住...<specific exception>
  • 很抱歉,没有 RAII 的 C++ 不是 C++。
  • 因此,您的奶牛工人摒弃了为解决某个问题而发明的技术,然后争论应该使用哪种劣质替代方案?很抱歉,但这似乎愚蠢,无论我怎么看。
  • “捕捉......而不重新抛出确实是错误的” - 你错了。在main 中,catch(...) { return EXIT_FAILURE; } 很可能在不在调试器下运行的代码中是正确的。如果你没有抓住,那么堆栈可能不会被展开。只有当您的调试器检测到您希望它们离开 main 的未捕获异常时。
  • ... 所以即使是“编程错误”,也不一定意味着您不想知道它。无论如何,你的同事都不是优秀的软件专业人士,所以正如 sbi 所说,很难谈论如何最好地处理一开始就长期虚弱的情况。

标签: c++


【解决方案1】:

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

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

std::vector 这样的类到底如何假装知道复制构造函数将抛出什么,同时仍然保证异常安全?

如果你总是知道被调用者在编译时会做什么,那么多态性将毫无用处!有时整个目标是抽象出较低级别发生的事情,因此您特别想知道发生了什么!

【讨论】:

  • 实际上,即使他们知道会抛出异常。这个代码重复的目的是什么?除非处理方式不同,否则我认为列举异常来炫耀您的知识毫无意义。
  • @MichaelKrelin-hacker:也是。此外,还要加上他们弃用异常规范的事实,因为在代码中列出所有可能的异常往往会在以后导致错误......这是有史以来最糟糕的想法。
  • 而困扰我的是,当将有用且方便的技术视为“无用的学校东西”时,这种态度的起源可能是什么。不过好吧……
  • +1,所有可能的选项的枚举是未来失败的绝佳秘诀,为什么有人会选择这样做而不是...
  • 不错的答案。如果一个必须支持的编译器在区域 X 中存在错误,那么使用区域 X 的功能并不聪明,至少不直接使用它可能会受益。例如,考虑到公司的信息,如果他们使用 Visual C++ 6.0,我不会感到惊讶,它在这方面有一些愚蠢的错误(比如异常对象析构函数被调用两次)——那些早期错误的一些较小的后代幸存下来这一天,但需要仔细安排才能显现。
【解决方案2】:

你似乎陷入了一个特定的地狱,有人试图吃蛋糕并吃掉它。

RAII 和异常旨在齐头并进。 RAII 是您没有编写大量catch(...) 语句来进行清理的方法。这是理所当然的,它会自动发生。并且异常是使用 RAII 对象的唯一方法,因为构造函数只能成功或抛出(或将对象置于错误状态,但谁愿意呢?)。

catch 语句可以做以下两件事之一:处理错误或异常情况,或进行清理工作。有时两者兼而有之,但每个catch 语句都存在至少做其中之一。

catch(...) 无法进行正确的异常处理。你不知道异常是什么;您无法获取有关异常的信息。除了 something 在某个代码块中抛出异常这一事实之外,您绝对没有任何信息。在这种情况下,您唯一可以做的合法事情就是进行清理。这意味着在清理结束时重新抛出异常。

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

所以我同意catch(...) 主要是邪恶的...... 暂时

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

你不能没有另一个。你不能说 RAII catch(...) 都不好。您至少需要其中之一;否则,你就不是异常安全的。

当然,catch(...) 有一个有效但罕见的用法,即使 RAII 也无法消除:让 exception_ptr 转发给其他人。通常通过promise/future 或类似接口。

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

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

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

这个概念让我感到困惑的是,它通常与大多数人认为 RAII 不好的说法相反。一般来说,参数是“RAII 不好,因为你必须使用异常来表示构造函数失败。但你不能抛出异常,因为它不安全,你必须有很多 catch 语句来清理所有内容。 "这是一个错误的论点,因为 RAII 解决了 RAII 的缺乏造成的问题。

他很可能反对 RAII,因为它隐藏了细节。析构函数调用不会立即在自动变量上可见。所以你得到了被隐式调用的代码。有些程序员真的很讨厌这样。显然,他们认为拥有 3 个 catch 语句,所有这些语句都使用复制和粘贴代码做同样的事情是一个更好的主意。

【讨论】:

  • 看来你写的代码没有提供strong异常安全保证。 RAII 有助于提供基本保证。但是为了提供强有力的保证,您必须撤消一些操作以将系统恢复到调用函数之前的状态。基本保证是cleanup,强保证是rollback。回滚是特定于功能的。所以你不能把它放到“RAII”中。这就是 catch-all 块变得方便的时候。如果你写的代码有很强的保证,你会使用catch-all
  • @anton_rh:也许,但即使在这些情况下,catch-all 语句也是最后的手段的工具。首选的工具是在您更改任何必须在异常时恢复的状态之前完成所有抛出 的操作。显然,您不能在所有情况下都以这种方式实现所有内容,但这是获得强大异常保证的理想方式。
【解决方案3】:

两个厘米,真的。第一个是,在一个理想的世界里,你 应该总是知道可能会抛出什么异常,在实践中,如果 您正在处理第三方库,或使用 Microsoft 进行编译 编译器,你没有。然而,更重要的是;即使你知道 正是所有可能的例外,这与这里有关吗? catch (...)catch ( std::exception const& ) 表达的意图要好得多,即使假设所有可能的异常都来自 std::exception(在理想世界中就是这种情况)。至于 使用几个 catch 块,如果没有共同的基础 例外:那是彻头彻尾的混淆,是维护的噩梦。 你如何认识到所有的行为都是相同的?然后 那是意图吗?如果你必须改变 行为(例如错误修复)?很容易错过。

【讨论】:

  • 实际上,我的同事设计了他自己的异常类,它不是派生自std::exception,并且每天都在尝试在我们的代码库中强制使用它。我的猜测是,他试图惩罚我使用不是他自己编写的代码和外部库。
  • @ereOn 在我看来,您的同事急需培训。无论如何,我可能会避免使用他编写的库。
  • 模板和知道什么异常会被抛出,就像花生酱和死壁虎一样。像std::vector<> 这样简单的东西几乎可以出于任何原因抛出任何类型的异常。
  • 请告诉我们,您怎么知道明天的错误修复会在调用树的下方进一步抛出什么异常?
【解决方案4】:

我认为您的同事已经混淆了一些好的建议 - 您应该只在不重新抛出它们的情况下处理 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;
}

很好,甚至是最佳实践,处理一般错误的代码应该与导致它们的代码一起使用。这比依赖被调用者知道事务需要回滚或其他什么要好。

【讨论】:

    【解决方案5】:

    的任何回答都应附有理由说明为什么会这样。

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

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

    编写catch(...) 来消除所有异常是错误的,因为您应该只处理您知道如何处理的异常,并且使用该通配符可以比您​​预期的更多,这样做可以消除重要错误。

    但如果您在 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");
    }
    

    【讨论】:

    • 希望Log(...)不能扔。
    【解决方案6】:

    我一般同意这里帖子的情绪,我真的不喜欢捕获特定异常的模式——我认为它的语法还处于起步阶段,还不能处理冗余代码。

    但是既然每个人都这么说,我会插嘴说,即使我很少使用它们,我也经常查看我的一个“catch(Exception e)”语句并说“该死,我希望我'd call out the specific exceptions that time" 因为当你稍后进来时,通常很高兴知道意图是什么以及客户可能会抛出什么。

    我并不是在为“始终使用 x”的态度辩护,只是说偶尔看到它们被列出确实很高兴,我相信这就是为什么有些人认为这是“正确”的方式。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-05-27
      • 1970-01-01
      • 1970-01-01
      • 2013-11-24
      • 1970-01-01
      • 1970-01-01
      • 2010-10-01
      相关资源
      最近更新 更多