Domanda

Exception safety is really important in Modern C++.

There is already a great question about exception safety here. So I am not talking about Exception safety in general. I am really talking about exception safety with Qt in C++. There is also a question about Qt Exception safety on Stack Overflow and we have the Qt documentation.

After reading everything I could find about exception safety with Qt, I really feel like it is very hard to achieve exception safety with Qt. As a result I am not going to throw any kind of exceptions myself.

The real problem is with std::bad_alloc:

  • The Qt documentation states that Throwing an exception from a slot invoked by Qt's signal-slot connection mechanism is considered undefined behaviour, unless it is handled within the slot.
  • As far as I know, any slot in Qt could throw a std::bad_alloc.

It seems to me that the only reasonable option is to exit the application before the std::bad_alloc is thrown (I really do not want to go into undefined behavior land).

A way to achieve this would be to overload operator new and:

  • if an allocation failure occures in the GUI thread: exit (kill) the application.
  • if an allocation failure occures in another thread just throw a std::bad_alloc.

Before writing that operator new I would really appreciate some feedback.

  1. Is it a good idea ?
  2. Will my code be exception safe this way ?
  3. Is it even possible to write exception safe code with Qt ?
È stato utile?

Soluzione

This problem has been long solved and has an idiomatic solution in Qt.

All slot calls ultimately originate either from:

  • an event handler, e.g.:

    • A timer's timeout signal results from the QTimer handling a QTimerEvent.

    • A queued slot call results from the QObejct handling a QMetaCallEvent.

  • code you have full control over, e.g.:

    • When you emit a signal in the implementation of main, or from QThread::run, or from QRunnable::run.

An event handler in an object is always reached through QCoreApplication::notify. So, all you have to do is to subclass the application class and reimplement the notify method.

This does affect all signal-slot calls that originate from event handlers. Specifically:

  1. all signals and their directly attached slots that originated from event handlers

    This adds a per-event cost, not a per-signal cost, and not per-slot cost. Why is the difference important? Many controls emit multiple signals per a single event. An QPushButton, reacting to a QMouseEvent, can emit clicked(bool), pressed() or released(), and toggled(bool), all from the same event. In spite of multiple signals being emitted, notify was called only once.

  2. all queued slot calls and method invocations

    They are implemented by dispatching a QMetaCallEvent to the receiver object. The call is executed by QObject::event. Since event delivery is involved, notify is used. The cost is per-call-invocation (thus it is per-slot). This cost can be easily mitigated, if desired (see implementation).

If you're emitting a signal not from an event handler - say, from inside of your main function, and the slot is directly connected, then this method of handling things obviously won't work, you have to wrap the signal emission in a try/catch block.

Since QCoreApplication::notify is called for each and every delivered event, the only overhead of this method is the cost of the try/catch block and the base implementation's method call. The latter is small.

The former can be mitigated by only wrapping the notification on marked objects. This would need to be done at no cost to the object size, and without involving a lookup in an auxiliary data structure. Any of those extra costs would exceed the cost of a try/catch block with no thrown exception.

The "mark" would need to come from the object itself. There's a possibility there: QObject::d_ptr->unused. Alas, this is not so, since that member is not initialized in the object's constructor, so we can't depend on it being zeroed out. A solution using such a mark would require a small change to Qt proper (addition of unused = 0; line to QObjectPrivate::QObjectPrivate).

Code:

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

Note that I completely ignore whether it makes any sense, in any particular situation, to handle std::bad_alloc. Merely handling it does not equal exception safety.

Altri suggerimenti

You don't need something as complex as overloading operator new. Create a class ExceptionGuard whose destructor checks std::uncaught_exception. Create this object in each slot, with automatic duration, outside any try-catch block. If there's an exception that still escapes, you can call std::terminate just before you'd otherwise return to Qt.

The big benefit is that you can place it in just the slots, not every random call to new. The big downside is that you can forget to use it.

BTW, it's not strictly necessary to call std::terminate. I'd still advice to do so in ExceptionGuard because it's intended as a last resort. It can do application-specific cleanup. If you have cleanup behavior specific to the slot you'd better do that outside ExceptionGuard, in a regular catch block.

Is it a good idea ?

It's unnecessary and needlessly complex. There are a lot of problems with trying to handle std::bad_alloc:

  • when it is thrown, there typically isn't much you can do about it. You're out of memory, anything you try to do might easily fail again.
  • in many environments, out-of-memory situations might occur without this exception being thrown. When you call new the OS just reserves a part of your (huge, 64-bit) address space. It doesn't get mapped to memory until much later, when you try to use it. If you're out of memory, then that is the step that will fail, and the OS won't signal that by throwing a C++ exception (it can't, because all you tried to do was read or write a memory address). It generates an access violation/segfault instead. This is the standard behavior on Linux.
  • it adds complexity to a situation that might already be tricky to diagnose and debug. Keep it simple, so that if it happens, your code won't do anything too unexpected that ends up hiding the problem or preventing you from seeing what went wrong.

Generally speaking, the best way to handle out-of-memory situations is just to do nothing, and let them take down the application.

Will my code be exception safe this way ?

Qt frequently calls new itself. I don't know if they use the nothrow variant internally, but you'd have to investigate that.

Is it even possible to write exception safe code with Qt ?

Yes. You can use exceptions in your code, you just have to catch them before they propagate across signal/slot boundaries.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top