Portable Use And Disabling Of Exceptions

Started by
30 comments, last by Bacterius 11 years, 2 months ago

Also, what is your opinion on defining new exception classes? Do you always extend a standard exception class, or do you define your own base class? Additionally, do you define all of your own exceptions, or do you only define new exceptions that don't cover what the standard ones don't?

Advertisement

Here's an alternative to throwing exceptions for game development purposes:

1. Any error that's caused by bad code, or is unrecoverable, should call a custom assert() macro so the program halts immediately (when IsDebuggerPresent() returns true) so the author of the bad code can see what's wrong and fix it. Any build that's not a final one should report the issue to the tester somehow so they can submit a bug report. Ideally you'd save out a crash dump to go in the bug report.

2. Any error that is caused by bad data should (in non-final builds) inform the user at runtime e.g. with MessageBox() for at least the first instance of that error so that someone who adds bad data and then tries to run the game can identify what's bad with their data and fix it. The game should then work round the error, e.g. by returning some default data that visually represents an error. There's not much data in a game that can't be replaced reliably like that. You could also have a button on the message box to allow a tester to treat it as if it was a fatal error.

3. For error conditions where it's unclear, go for #2 if possible.

Generally this will all be done via game engine code, so a portable library could expose error / warning callbacks, and provide a default implementation that does something simple and portable (e.g. call printf() and assert(0)).

The D3D11 implementation has a similar approach, but it's overkill for a small library. It lets you disable specific warnings, and ask it to break in the debugger on the error types you're interested in.

I used to be a huge fan of assertions, though exceptions speak to me in a higher-level sense.

Another problem that I encounter, is that if I am going with the rationale that I'd disable exceptions in a release build, then no behavior that relies on exceptions being caught can be used, because attempting to swallow an exception will instead never get called, and the application would terminate, or whatever the compiler does decide to do. Thus, exceptions can only be used for fatal circumstances, resulting in behavior that is like glorified assertions.

Can anyone provide a reason to use exceptions over assertions, given that exceptions may be disabled at any point? I feel that I should use exceptions, to have code that will provide added functionality if the application is compiled with exceptions enabled in all builds, like an application that isn't relying on the fastest execution time.

Exceptions and assertions are for different things.


Use exceptions if you need your program's flow to reactively change to unexpected circumstances. Use assertions if you realize that some invariant or expectation has been violated and terminating the program is preferable to continuing blindly into whatever chaos may result. These are orthogonal concerns.


Exception support generally sucks on the current console platforms, btw, and is almost universally disabled for performance-intensive projects. My general rule is: assertions should kill the program and dump core. Anything that would normally trip an exception should kill the program and dump core. Recoverable errors should be handled in that the design of the system should elegantly allow for the program to continue and, well, recover. If all of your target platforms have exceptions and you are very good at exception safe design in C++ and you like them, go ahead and use them. If any of those things fails to be true, don't mess with them (in C++ specifically) because you're liable to blow something up if and when a real error does occur.

I'm on the side of forbidding the use of exceptions entirely in C++, FWIW.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

I'm still wavering heavily on the performance concerns, but there's still one way that I can think of exceptions as negating the speed penalty: propagating return codes. I used to spend so much time checking if a return code indicates success, and if not, clean up and return the error to the caller. Then the caller checks if the return code indicates success, and if not, clean up and return the error to the caller... ad infinitum.

Propagating error codes made code much less readable, and it causes you to perform a ton of conditional checks repeatedly to ensure that no error occurs.

It is not hard to imagine that checking error codes results in more time being spent checking for errors during successful execution than when an error occurs; handling an error is very fast to propagate the error, and then have someone take responsibility and do something about the error at the top of the call chain. However, there is an extremely larger percentage of time where an error does not occur. As a result, all of these conditionals to check each and every return code, not even to handle the error but to propagate it up the call stack to the code that will finally handle the error code.

This tells me that even while there is a penalty for using exceptions, it might not be any worse than checking error codes for every little thing.

The only way I can think of eliminating all error checking overhead is to make almost every error a fatal one, where it doesn't get propagated at all, at the expense of being unable to recover from a possibly recoverable error.

I'm still wavering heavily on the performance concern

The performance issues are very different, depending on whether we are discussing consoles, or general purpose PCs.

Consoles tend to have abysmal performance when it comes to exceptions. In part this is because the compilers and runtimes have not been extensively optimised for the use of exceptions, and in part because consoles operate within strict hardware limitations.

If you are only targeting full-blown PCs, then the discussion is largely moot, because you'd have to be using exceptions to simulate control flow statements before they became a noticeable performance hotspot...

---

That said, I'm not a fan of exceptions. They can't be thrown across the boundary between threads, processes, hosts or programming languages. And anywhere in your program, you can can never tell if a function call might result in an arbitrary exception being thrown.

They have been termed "invisible gotos", and I don't think that is an unfair characterisation.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

I do like the concept, when used properly. I agree that it isn't easy to know when a function will throw an exception, unless you check its documentation. However, I'd argue that it's just as hard to tell if it will trigger an assertion, when used in place of exceptions.

However, I'd argue that it's just as hard to tell if it will trigger an assertion, when used in place of exceptions.

Every function should be a candidate for triggering an assertion, but that doesn't matter because the caller doesn't need to be aware of this. The caller just needs to be aware of any pre-conditions -- e.g. "index must be smaller than size" -- and know that if these mustn't be violated. I find it cleaner to say that these things can't be violated "or else!" (so that in the retail version you can remove all the error checking code) rather than saying that if they are violated then an error will be returned (because then the error handling is part of the interface, and can't be removed in case a caller relies on it).

With exceptions, the caller needs to know what kinds of assertions can be thrown under which circumstances, so that they can either handle them, or add them to their own documentation and pass the buck to their own caller. Failure to do so will result in an unhandled exception, with your program unwinding to main, so callers need to know.

With assertions, if something is broken, then you get a crash dump / debug breakpoint so that you can fix your code, which is alerting the developer, not the caller.

The only way I can think of eliminating all error checking overhead is to make almost every error a fatal one, where it doesn't get propagated at all, at the expense of being unable to recover from a possibly recoverable error.

Do you have any examples of a recoverable error that you want to support? e.g. in my experience, on consoles if you get a std::bad_alloc, then you've already lost -- if the program needs 600MiB of RAM to run, then it can't run, and needs the attention of the developers, there's nothing a catch block can do to fix the game.


My most frequent use-case for exceptions is to indicate something that should not happen in a normal work load has happened (out of memory, data that goes into an internal structure from external code is invalid, attempting to use something that is in a valid but explicitly unusable state). In these cases, I would use a debugger to figure it out, but swallowing exceptions is against my style, so they would only occur in areas that must be fixed; a perfect run should never throw them, but still have the option of handling them just in case, like the extremely rare circumstance of expecting memory allocation to fail if you allocate based on input, and displaying an error asking for a different value, though nothrow new could just as easily be used.

May I ask why you can't just use assert()?


QFE -- for any kind of error that requires the attention of a developer in order to fix it, then exceptions are harmful. When you throw an exception, you're unwinding the call-stack, which is like ordering a clean-up crew to a crime-scene before forensics have arrived. In order to debug the issue, you need to freeze the crime scene as soon after the illegal operation as you can.

I still maintain that exceptions and assertions are orthogonal concepts.

An assertion says "something happened which emphatically should not be possible" - a contract violation, or something fundamentally broken in the code of some sort. Pinpointing the cause of this failure and preserving the complete context of the assertion failure is paramount. You can and should dump core - any reasonable user-facing system should automatically submit the complete callstack and (ideally) a minidump to the developer. Any middleware should gracefully fail with one of two kinds of information: a complete description of the nature of the assertion and what, if anything, the caller did to trip it; or a comprehensive location identification and relevant state so that the middleware author can locate, ideally reproduce, and then fix the internal cause of failure.

Exceptions are totally different. An exception says "something happened which I cannot handle in this current control flow context." The implicit assumption is that someone higher up the call stack can deal with the situation. If you do not comprehensively catch exceptions when they can possibly be thrown (accidental oversight aside), you are using exceptions wrong.

Earlier in this thread, someone made the remark that having a "disabled" exception call abort() is a reasonable tradeoff. I think this reflects a completely misguided interpretation of how exceptions are meant to be used. Exceptions should be thought of as recoverable in almost all circumstances. If you want a recoverable error, the right thing to do is to catch the exception at the appropriate contextual site and recover. If you want to kill the program, you should dump core and immediately exit, period. Unwinding the stack in this scenario is a sin and you should be dragged behind the metaphorical woodshed and beaten to within an inch of your life if you ship software that unwinds the stack and then abnormally terminates. Put another way, having a try/catch in main() is a crime against humanity.

If we're talking about a middleware product, for instance, I should also get consistency across platforms; if I compile your code with exceptions off, I shouldn't get abnormal exits left and right when recoverable errors occur. By the same token, if I'm used to your code dumping core when I do something wrong, I shouldn't port to a new platform and suddenly get your exceptions getting thrown instead.

In fact, if this is for a C++ middleware product, exceptions shouldn't even be considered an option. The boundaries and limitations on exception propagation have been hinted at already, but it cannot be overstated that these will cause your users to have painful problems. Even header-only libraries should avoid C++ exceptions because you can't guarantee that the client code is exception safe (or even has them turned on).

Genuine "oh shit" moments should assert and dump core, end of story. Recoverable errors should be propagated by return or out-parameter or some other easily retrievable status field, depending on your preferred style.

If this is not middleware, it's really up to your preferences, but please don't abuse exceptions, especially not in C++.

If this is not C++, then we need to re-frame the discussion, because other languages have occasionally managed to not suck at exceptions as badly as C++ does :-)

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]


Failure to do so will result in an unhandled exception, with your program unwinding to main, so callers need to know.


I definitely agree with this.


Do you have any examples of a recoverable error that you want to support?

My guess would be benign ones like out_of_range. I haven't designed any exceptions yet, because I'm still on the fence about what they would be used for, if anything, so I have a hard time answering that one.

To be honest, I have a hard time deciding which path I should go, because I'm hearing conflicting viewpoints in this thread, and around various other blogs, fora, and reference sites. Some people say to never throw, and to die immediately in every bad circumstance. Others say that most talks of side-effects of exceptions are ill-informed, and there's nothing wrong with using them for performance. I would personally subscribe to a three-pronged approach, but then there is a problem that code that interacts with this library might not have exceptions enabled, and thus I can't use exceptions for recoverable things, because this will result in abortion. Then there's the problem that unwinding the stack for fatal problems is nonsensical, so using them in place of assertions makes no sense. So, if I can't guarantee that exceptions can be used, they seem to get underfoot. However, having the execution terminate at the first sign of unexpected trouble seems like an unfriendly policy for other code that uses this library.

So, I'll entertain this thought pattern, too. If assertions are used for everything that isn't useful and expected behavior, how does this affect the usage of constructors and RAII principles? Does this not affect one's methodologies if they swap from using exceptions if a fatal error occurs in a constructor? I also feel like this would create the strange requirement that a custom allocator class should trigger an assertion if it fails to allocate memory, like new would if exceptions are turned off.

This topic is closed to new replies.

Advertisement