3

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 ?
Community
  • 1
  • 1
Arnaud
  • 3,765
  • 3
  • 39
  • 69
  • 2
    Why do you want to handle `std::bad_alloc` at all? If you run out of memory, there isn't generally much you can do to handle it *anyway*. Just let it take down the application. – jalf Mar 18 '14 at 09:21
  • Why do you think any slot can throw `std::bad_alloc`? What if there is no dynamic allocation? – lapk Mar 18 '14 at 09:22
  • @jalf Because of the undefined behavior part. – Arnaud Mar 18 '14 at 09:23
  • @PetrBudnik Of course if there is not memory allocation everything is fine. What I really meant was many slots have to allocate some memory and could throw. – Arnaud Mar 18 '14 at 09:26
  • 1
    It doesn't matter. The behavior is not defined by Qt, but it is well defined by the C++ standard: it is an unhandled exception, and those terminate the program. So yes, Qt becomes unreliable, which doesn't matter because you're not able to *do* anything with it anyway. – jalf Mar 18 '14 at 09:26
  • @Arnaud Just catch all exceptions within slot and don't let then propagate outside of it. – lapk Mar 18 '14 at 09:30
  • @jalf I really think they meant **Undefined Behavior**. What if during the stack unwinding the program tries to touch something it should not ? We know that Qt is not exception safe. – Arnaud Mar 18 '14 at 09:42
  • Exception safety doesn't imply not killing the application. Exception safety only means that the resources that need to be released, are released. Killing the application is a valid response to an exception that cannot be meaningfully otherwise handled. – Kuba hasn't forgotten Monica Mar 18 '14 at 17:06
  • @KubaOber You are right. The only reason I want to kill the application is because of the undefined behavior part. But apparently, the documentation is misleading and there is an idiomatic way to handle those bad_allocs (+1 for showing it to me BTW). – Arnaud Mar 18 '14 at 17:18

3 Answers3

4

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.

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • +1 because it is indeed recommended by Qt itself: "Qt has caught an exception thrown from an event handler. Throwing exceptions from an event handler is not supported in Qt. You must reimplement QApplication::notify() and catch all exceptions there." Of course, why Qt didn't do this in the first place is baffling. It is a basic UX anti-pattern: tell your users to do Foo when you can perfectly well do Foo yourself. – MSalters Mar 18 '14 at 16:08
  • @MSalters The user may not wish to catch all exceptions. It'd be an error for Qt to do so. I agree that it would be a UX anti-pattern if indeed it was possible for Qt to do it. As it is, Qt can't know what the users wish to do with exceptions. It's perfectly fine, nah, desired, in fact, for some exceptions to be fatal. – Kuba hasn't forgotten Monica Mar 18 '14 at 16:15
  • Sorry, doesn't make sense to me. If it's "unsupported by Qt", the users wish doesn't matter. They *must* handle that exception. Obviously, Qt can trivially implement a `virtual void OnException(std::exception&)` method if it wanted a well-defined but overridable implementation. That virtual call overhead really doesn't matter once you have thrown an exception. – MSalters Mar 18 '14 at 16:28
  • @MSalters Not all exceptions need to derive from `std::exception`. Yes, `std::bad_alloc` does, but why limit this to stuff thrown by the standard library. It is, in fact, impossible to implement it generically in a virtual method. That's probably why it wasn't done. And the signature would need to be something like `virtual bool onException(std::exception &)`, where a false result rethrows the exception. – Kuba hasn't forgotten Monica Mar 18 '14 at 17:38
  • 1
    @KubaOber The parameter could be a std::exception_ptr. – Arnaud Mar 18 '14 at 22:39
3

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.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • While this is some form of a solution, it misses the idiomatic way of handling this in Qt, and ignores how control ends up in a slot. It is quite error prone, since such a guard is trivial to forget. Moreover, it always has runtime cost, whether the exception was thrown or not. It seems valid, but just too bad to worth even mentioning. – Kuba hasn't forgotten Monica Mar 18 '14 at 15:51
  • Wait a minute - you propose to override `QCoreApplication::notify`, which affects _every_ signal/slot call, and then point out that there's a runtime cost in my solution (which is just `std::uncaught_exception`, not something very expensive) ?! As for being able to forget it, that is the price paid for *any* method which is optional. Any solution that's non-optional is by definition called in many cases where it is useless. – MSalters Mar 18 '14 at 15:59
  • A try-catch block is needed no matter what. An instantiation of an object isn't. It's also not true that this affects all signal-slot calls. It only affects emission of signals that are caused by events. Those signals are the problematic ones, since the locus of control originates in Qt, not in your own code. But you raise a valid point, I'll amend my answer with a way of doing it more discriminately. – Kuba hasn't forgotten Monica Mar 18 '14 at 16:17
  • @KubaOber: Why is a try-catch block needed? Remember that the question is how to end the application cleanly before that ` std::bad_alloc` (or other) exception escapes to Qt. – MSalters Mar 18 '14 at 16:30
1

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.

jalf
  • 243,077
  • 51
  • 345
  • 550