[Development] Exceptions

Volker Hilsheimer volker.hilsheimer at qt.io
Wed May 22 16:30:33 CEST 2024


> On 22 May 2024, at 13:20, Turtle Creek Software <support at turtlesoft.com> wrote:
> 
> When we posted this problem to the Interest list, Thiago replied that Qt is not designed to pass along exceptions and we shouldn't rely on the override to QApplication::notify.  He later qualified that some parts of Qt should be exception-safe but QWidget etc are not.

Taking a deeper look at this, the TL;DR is that on ARM ABI, a function compiled without exception support will result in std::terminate being invoked if an exception is thrown, which invalidates our documentation as well as your architecture.


The two main points are:


1) “as long as Qt is built with exception support”.

Checking our build system: most of Qt does not get built with -fexceptions; Qt Core is:

qt_internal_add_module(Core
    QMAKE_MODULE_CONFIG moc resources
    NO_GENERATE_METATYPES # metatypes are extracted manually below
    EXCEPTIONS
    SOURCES
...


but Qt Widgets, Qt Gui, and generally most other modules are not:


qt_internal_add_module(Widgets
    QMAKE_MODULE_CONFIG uic
    PLUGIN_TYPES styles
    SOURCES
…


So, we evidently compile most of Qt without exceptions enabled. The compile will not generate any code related to exception handling. We explicitly set -fexception for the former, and explicilty set -fno-exceptions for the latter, so Widgets wouldn’t be built with exception handling on either architecture. This is nothing new, and it doesn’t explain why things don’t work for you on ARM, but worked on x86. Either way, Thiago is correct in saying that things happened to work by chance, not by design.

Changing the default or turning exception handling on for all modules requires modifying the build system, and you end up with an entirely untested Qt build. But it’s perhaps worth exploring, both to evaluate the (presumably significant) impact on binary size and performance, and to see if that restores the behavior you are expecting.


> I don't know enough about Qt's guts to understand why that is the case. From what you say, maybe it isn't.



The next question is then: what can you expect if you build all of Qt with exceptions enabled? Which brings us to the second point:

2) exception safety

Our documentation at https://doc.qt.io/qt-6/exceptionsafety.html documents that "the only supported use case for recovering from exceptions thrown within Qt (for example due to out of memory) is to exit the event loop and do some cleanup before exiting the application.”, by putting the call to Q(Core)Application::exec into a try/catch block.

Qt never throws exceptions, but your application might. So the question is then: what should happen if application code called by Qt throws an exception?


In most cases, all application code (with the exception of your start-up code) is called by an event handler that is called by Qt (and your stack trace starts somewhere in Q(Core)Application::notify). Either you connected a slot to a signal that gets emitted by Qt, or you overrode an event handler or similar virtual function. That signal might get emitted, and the virtual function might get called, in the middle of a longer function that does some work before and after. If your code throws an exception, then the “work after” won’t happen, because (most of) Qt doesn’t handle exceptions. The stack will unwind until the end, or until a catch block is reached. So what guarantees can Qt provide that objects are in a predictable, defined state if your code throws an exception?

The simple answer is: none. The state of the object will be undefined, and sometimes fatally so. Hence, the only reliable strategy for your application is to terminate after some cleaning up that doesn’t rely on an application object states.


All that said, a local experiment shows that this what we suggest in our documentation doesn’t work on macOS ARM. See below.

> Likewise, I'm not a good enough programmer to understand how any given compiler implements throw/catch and how it might go wrong.  In theory a throw should unwind the stack, and either stop at a catch or terminate if none are hit.  I looked at the source for a few Qt classes and didn't find any noexcepts. However, AFAIK that is just a compiler suggestion that an exception won't be thrown, and it shouldn't affect passage of throws from callers.


In Qt 6 code, we use noexcept quite extensively if we can guarantee that no exception gets thrown (which typically means that no memory gets allocated; so move- and copy-constructors of implicitly shared classes will typically be noexcept). For functions declared with noexcept, the compiler will not generate the unwinding code, no matter whether compiled with -fexception or not.

When a noexcept function throws in C++17, std::terminate is invoked. This is defined behavior.

When code throws an exception and some of the code in between has not been compiled with exception handling enabled - that is the situation here - then I think the behavior is implementation specific. And that is perhaps what you are experiencing. ARM states in [1]

"Disabling function unwinding for a function has the following effects

- Exceptions cannot be thrown through that function at runtime, and no stack unwinding occurs for that throw. If the throwing language is C++, then std::terminate is called.
“

and in [2]

"In C++, when the --no_exceptions option is specified, throw and try/catch are not permitted in source code. However, function exception specifications are still parsed, but most of their meaning is ignored.
In C, the behavior of code compiled with --no_exceptions is undefined if an exception is thrown through the compiled functions. You must use --exceptions, if you want exceptions to propagate correctly though C functions."

(nothing is said about what happens in C++ if an exception is thrown through a function compiled with —no_exceptions).

[1] https://developer.arm.com/documentation/dui0491/i/C-and-C---Implementation-Details/C---exception-handling
[2] https://developer.arm.com/documentation/dui0491/i/Compiler-Command-line-Options/--exceptions----no-exceptions?lang=en


What I observe in a local test on macOS ARM: if a Qt function (built without exception handling) is involved in the stack, then the throw expression will go straight std::terminate:

libc++abi.dylib!std::__terminate(void (*)()) (Unknown Source:0)
libc++abi.dylib!__cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) (Unknown Source:0)
libc++abi.dylib!__cxa_throw (Unknown Source:0)
sandbox!Widget::mousePressEvent(QMouseEvent*) (/Users/vohi/qt/testing/sandbox/main.cpp:24)

So my assumption is that C++ functions compiled with —no_exceptions are equivalent to functions explicitly declared as noexcept: if they (or a function they call) throw, then std::terminate is invoked.


> If this really is a bug, we can help debug it by adding throws at various places and looking at stack traces.  Maybe there's more we can do.  But the bug report we made for it was quickly closed as "invalid" by Thiago.  I'm also non-expert at internal politics at Qt!


From what I see, you, and we in our documentation, have been relying on compiler/libc++ runtime specific behavior that doesn’t hold true on ARM ABI. That’s at least my interpretation of what I’m seeing and what the ARM developer documentation states.

So, given how thoroughly your architecture relies on exception handling, it seems that you have to make sure that everything involved in your application is compiled with exception handling enabled. That still leaves the issues raised under point 2), but it might be a step forward for you.


Addressing point 2) systematically to the point where we could claim that Qt returns to a predictable state when application code throws would be, as pointed out by Henry, a massive, and in practice unverifiable, undertaking.


Cheers,
Volker

> Thanks, Dennis
> 
> On Wed, May 22, 2024 at 5:38 AM Volker Hilsheimer <volker.hilsheimer at qt.io> wrote:
> Hi Dennis,
> 
> > On 7 May 2024, at 21:05, Turtle Creek Software <support at turtlesoft.com> wrote:
> > 
> > TurtleSoft posted on the Interest list earlier about problems we had with exception handling, and Thiago suggested I post here.
> > 
> > Since the early 90s, our C++ code has had about 10,000 sanity checks which give an error message with source file & line number, then throw an exception. There is a try/catch block in the event loop which swallows them.  That way users can continue work.  Only a small % of the checks are ever called, but it makes bugs very easy to fix when users or testers report them. Our app almost never crashes thanks to the many checks. We learned the system from Bill Atkinson, author of MacPaint and HyperCard.
> > 
> > The sanity system started in Metrowerks PowerPlant, then worked OK in Carbon, Cocoa, and MFC.  For Qt we wrapped QApplication::notify with try/catch. It worked for most checks on Macintel and Wintel, but failed for Mac ARM. After the message, instant termination. Users lose their work, testers must start over.
> > 
> > Turns out that Qt intentionally does not support exceptions thrown through GUI classes, and the early success was just an accident.
> 
> 
> Can you elaborate on that point? As long as Qt is built with exception support, exceptions thrown in code that starts in the event loop should reach a QCoreApplication::notify override. There might be code paths (esp on macOS) that go into the native functions and back into Qt, and perhaps some of those native stack frames eat the exception or don’t pass it on otherwise. And there is code in Qt that catches exceptions thrown in constructors or destructors (including ~QWidget).
> 
> But in general, Qt shouldn't eat or disable or otherwise handle exceptions deliberately.
> 
> 
> 
> > Exceptions are an "exceptionally" handy C++ feature. Instant "beam me up, Scotty". It is too bad that Qt does not support them fully, and I'd like to change that. So I would be willing to work on getting GUI classes like QWidget able to pass exceptions, if it means we can get our old sanity-checking back. It would be our open-source contribution.
> 
> 
> Sadly, exceptions are perhaps also one of the most poorly designed features of C++. The status quo has resulted in several alternative approaches and strategies and a fragmented way of handling errors in general, which is (still?) subject of discussion in the C++ committee. E.g.
> 
> https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0709r4.pdf
> 
> (to be fair, I don’t know if this particular proposal is still on the table; std::expected in C++ 23 is one attempt at unifying error handling at least, but it’s a library rather than a language feature, so not nearly as capable especially for handling truly unexpected situations).
> 
> 
> > I realize this is a huge, fundamental change to Qt. It would need to be a multi-year project, done in tiny bits so as not to break things.  Anyhow, my time is limited.
> > 
> > Getting exceptions through signals/slots is lower priority. We don't use those much and probably could bypass them somehow. We just want a reliable path to the event loop.
> > 
> > Personally, I've programmed since punch card days in 1966 or so. I've designed and shipped several apps for PCs. I've refactored and rebuilt all sorts of C++ code from other people, and could do this work politely and competently.
> > 
> > Does this seem reasonable?
> 
> Let’s get to the bottom of the problem you are seeing first. IIRC, we tried to make Qt exception-safe in the Nokia/Symbian-Porting days, but that’s many years ago and those code paths have not gotten much interest in the time since.
> 
> Volker
> 
> 



More information about the Development mailing list