【问题标题】:Is there a way not to kill an Qt application which throwed a std::bad_alloc?有没有办法不杀死抛出 std::bad_alloc 的 Qt 应用程序?
【发布时间】:2014-04-23 20:53:54
【问题描述】:

异常安全在现代 C++ 中非常重要。

关于异常安全here 已经有一个很好的问题。 所以我不是在谈论一般的异常安全。我真的在谈论 C++ 中 Qt 的异常安全性。还有一个 question 关于 Stack Overflow 上的 Qt 异常安全性,我们有 Qt documentation

在阅读了有关 Qt 异常安全的所有内容后,我真的觉得使用 Qt 实现异常安全非常困难。因此,我自己不会抛出任何异常。

真正的问题在于 std::bad_alloc:

  • Qt 文档指出从 Qt 的信号槽连接机制调用的槽中抛出异常被视为未定义行为,除非它在槽内处理
  • 据我所知,Qt 中的任何插槽都可能抛出 std::bad_alloc。

在我看来,唯一合理的选择是抛出 std::bad_alloc 之前退出应用程序(我真的不想进入未定义的行为领域)。

实现这一点的一种方法是重载 operator new 和:

  • 如果在 GUI 线程中发生分配失败:退出(终止)应用程序。
  • 如果在另一个线程中发生分配失败,只需抛出 std::bad_alloc。

在写那个 operator new 之前,我非常感谢一些反馈。

  1. 这是个好主意吗?
  2. 这样我的代码会不会异常安全?
  3. 甚至可以用 Qt 编写异常安全代码吗?

【问题讨论】:

  • 你为什么要处理std::bad_alloc?如果您的内存不足,那么无论如何通常也无能为力。让它关闭应用程序。
  • 为什么你认为任何插槽都可以抛出std::bad_alloc?如果没有动态分配怎么办?
  • @jalf 因为未定义的行为部分。
  • @PetrBudnik 当然,如果没有内存分配一切都很好。我真正的意思是许多插槽必须分配一些内存并且可能会抛出。
  • 没关系。该行为不是由 Qt 定义的,但它由 C++ 标准很好地定义:它是一个未处理的异常,并且那些终止程序。所以是的,Qt 变得不可靠,这没关系,因为无论如何你都无法任何事情。

标签: c++ qt exception c++11 exception-safety


【解决方案1】:

这是个好主意吗?

这是不必要和不必要的复杂。尝试处理std::bad_alloc有很多问题:

  • 当它被抛出时,您通常无能为力。您内存不足,您尝试做的任何事情都可能再次失败。
  • 在许多环境中,可能会出现内存不足的情况,而不会引发此异常。当您调用new 时,操作系统只会保留一部分(巨大的,64 位)地址空间。直到很久以后,当您尝试使用它时,它才会映射到内存。如果您的内存不足,那么 that 是会失败的步骤,并且操作系统不会通过抛出 C++ 异常来发出信号(它不能,因为您尝试做的只是读取或写入内存地址)。相反,它会生成访问冲突/段错误。这是 Linux 上的标准行为。
  • 它增加了可能已经难以诊断和调试的情况的复杂性。保持简单,以便如果发生这种情况,您的代码不会做任何过于意外的事情,最终隐藏问题或阻止您看到问题所在。

一般来说,处理内存不足情况的最佳方法就是什么都不做,然后让它们关闭应用程序。

这样我的代码会不会异常安全?

Qt 经常调用new 本身。我不知道他们是否在内部使用 nothrow 变体,但您必须对此进行调查。

甚至可以用 Qt 编写异常安全的代码吗?

是的。您可以在代码中使用异常,只需在它们传播到信号/槽边界之前捕获它们。

【讨论】:

    【解决方案2】:

    您不需要像重载operator new 这样复杂的东西。创建一个类ExceptionGuard,其析构函数检查std::uncaught_exception。在任何 try-catch 块之外的每个插槽中创建此对象,并具有自动持续时间。如果仍有异常发生,您可以在返回 Qt 之前调用 std::terminate

    最大的好处是您可以将它放在插槽中,而不是每次随机调用new。最大的缺点是您可能会忘记使用它。

    顺便说一句,调用std::terminate 并不是绝对必要的。我仍然建议在ExceptionGuard 中这样做,因为它是最后的手段。它可以进行特定于应用程序的清理。如果您有特定于插槽的清理行为,您最好在 ExceptionGuard 之外的常规 catch 块中执行此操作。

    【讨论】:

    • 虽然这是某种形式的解决方案,但它错过了在 Qt 中处理此问题的惯用方式,并且忽略了控制如何在插槽中结束。它很容易出错,因为这样的守卫很容易忘记。此外,无论是否抛出异常,它总是有运行时成本。这似乎是有效的,但太糟糕了,甚至不值得一提。
    • 等一下 - 您建议覆盖 QCoreApplication::notify,这会影响 每个 信号/插槽调用,然后指出我的解决方案中存在运行时成本(即只是std::uncaught_exception,不是很贵的东西)?!至于能够忘记它,那是为可选的 any 方法付出的代价。根据定义,任何非可选的解决方案在很多情况下都是无用的。
    • 无论如何都需要一个 try-catch 块。对象的实例化不是。这也不会影响所有信号槽调用。它只影响由事件引起的信号的发射。这些信号是有问题的,因为控制点源自 Qt,而不是您自己的代码。但是你提出了一个有效的观点,我会用一种更有区别的方式来修改我的答案。
    • @KubaOber:为什么需要 try-catch 块?请记住,问题是如何在 std::bad_alloc (或其他)异常逃逸到 Qt 之前干净地结束应用程序。
    【解决方案3】:

    这个问题早就解决了,在Qt中有一个惯用的解决方案。

    所有槽调用最终都来自:

    • 一个事件处理程序,例如:

      • 定时器的timeout 信号是由QTimer 处理QTimerEvent 产生的。

      • QObejct 处理QMetaCallEvent 会导致排队槽调用。

    • 您可以完全控制的代码,例如:

      • 当您在执行main、或来自QThread::run、或来自QRunnable::run 时发出信号。

    始终通过QCoreApplication::notify 访问对象中的事件处理程序。因此,您所要做的就是继承应用程序类并重新实现 notify 方法。

    这确实会影响源自事件处理程序的所有信号槽调用。具体来说:

    1. 所有信号及其直接附加源自事件处理程序

      这增加了每个事件的成本,不是每个信号的成本,不是每个槽的成本。为什么差异很重要?许多控件在单个事件中发出多个信号。 QPushButtonQMouseEvent 作出反应,可以发出 clicked(bool)pressed()released()toggled(bool),所有这些都来自同一事件。尽管发出了多个信号,notify 只被调用了一次。

    2. 所有排队槽调用和方法调用

      它们是通过将QMetaCallEvent 分派给接收者对象来实现的。调用由QObject::event 执行。由于涉及到事件传递,所以使用notify。成本是每个调用调用(因此它是每个插槽)。如果需要,可以轻松降低此成本(请参阅实施)。

    如果您不是从事件处理程序发出信号 - 例如,从您的 main 函数内部,并且插槽是直接连接的,那么这种处理事情的方法显然不起作用,您必须包装try/catch 块中的信号发射。

    由于每个交付的事件都会调用QCoreApplication::notify,因此该方法的唯一开销是try/catch 块的成本和基本实现的方法调用。后者很小。

    可以通过仅将通知包装在标记对象上来缓解前者。这需要在不影响对象大小的情况下完成,并且不涉及在辅助数据结构中的查找。这些额外成本中的任何一个都将超过没有抛出异常的 try/catch 块的成本。

    “标记”需要来自对象本身。有一种可能性:QObject::d_ptr->unused。唉,事实并非如此,因为该成员没有在对象的构造函数中初始化,所以我们不能依赖它被清零。使用这种标记的解决方案需要对 Qt 进行适当的小改动(将unused = 0; 行添加到QObjectPrivate::QObjectPrivate)。

    代码:

    template <typename BaseApp> class SafeNotifyApp : public BaseApp {
      bool m_wrapMetaCalls;
    public:
      SafeNotifyApp(int & argc, char ** argv) : 
        BaseApp(argc, argv), m_wrapMetaCalls(false) {}
      void setWrapMetaCalls(bool w) { m_wrapMetaCalls = w; }
      bool doesWrapMetaCalls() const { return m_wrapMetaCalls; }
      bool notify(QObject * receiver, QEvent * e) Q_DECL_OVERRIDE {
        if (! m_wrapMetaCalls && e->type() == QEvent::MetaCall) {
          // This test is presumed to have a lower cost than the try-catch
          return BaseApp::notify(receiver, e);
        }
        try {
          return BaseApp::notify(receiver, e);
        }
        catch (const std::bad_alloc&) {
          // do something clever
        }
      }
    };
    
    int main(int argc, char ** argv) {
      SafeNotifyApp<QApplication> a(argc, argv);
      ...
    }
    

    请注意,我完全忽略了在任何特定情况下处理std::bad_alloc 是否有意义。仅仅处理它不等于异常安全

    【讨论】:

    • +1 因为确实是Qt自己推荐的:“Qt has catch an exception throw from an event handler.Qt不支持从事件处理程序中抛出异常。你必须重新实现QApplication::notify () 并在那里捕获所有异常。”当然,为什么 Qt 一开始不这样做是令人费解的。这是一个基本的 UX 反模式:告诉你的用户在你自己可以完美地做 Foo 时做 Foo。
    • @MSalters 用户可能不希望捕获所有异常。 Qt 这样做是错误的。我同意如果 Qt 确实有可能做到这一点,这将是一个 UX 反模式。事实上,Qt 无法知道用户希望对异常做什么。完全没问题,不,实际上,希望某些异常是致命的。
    • 对不起,对我来说没有意义。如果它“不受 Qt 支持”,那么用户的愿望并不重要。他们必须处理该异常。显然,如果 Qt 想要一个定义明确但可覆盖的实现,它可以轻松实现 virtual void OnException(std::exception&amp;) 方法。一旦抛出异常,虚拟调用开销就真的无关紧要了。
    • @MSalters 并非所有异常都需要派生自 std::exception。是的,std::bad_alloc 确实如此,但为什么要将其限制为标准库抛出的东西。事实上,在虚拟方法中一般地实现它是不可能的。这可能是它没有完成的原因。并且签名需要类似于virtual bool onException(std::exception &amp;),其中错误的结果会重新引发异常。
    • @KubaOber 参数可以是 std::exception_ptr。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-07-15
    • 2011-11-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-12
    • 1970-01-01
    相关资源
    最近更新 更多