3

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

这里已经有一个关于异常安全的大问题。所以我不是在谈论一般的异常安全。我真的在谈论 C++ 中 Qt 的异常安全性。Stack Overflow 上还有一个关于Qt 异常安全的问题,我们有 Qt文档

在阅读了我能找到的关于 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 编写异常安全代码吗?
4

3 回答 3

4

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

所有插槽调用最终都来自:

  • 事件处理程序,例如:

    • 计时器的timeout信号来自QTimer处理 a QTimerEvent

    • QObejct处理 a会导致排队的插槽调用QMetaCallEvent

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

    • main当您在, or from QThread::run, or from的实现中发出信号时QRunnable::run

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

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

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

    这增加了每个事件的成本,而不是每个信号的成本,也不是每个插槽的成本。为什么差异很重要?许多控件在单个事件中发出多个信号。An QPushButton, 对 a 的反应QMouseEvent可以发出clicked(bool), pressed()or released(), and toggled(bool), 都来自同一个事件。尽管发出了多个信号,但notify只调用了一次。

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

    它们是通过将 a 分派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. 仅仅处理它并不等于异常安全

于 2014-03-18T15:49:20.060 回答
3

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

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

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

于 2014-03-18T09:43:09.077 回答
1

这是个好主意吗 ?

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

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

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

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

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

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

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

于 2014-03-18T09:29:23.843 回答