C++ exceptions

Started by
52 comments, last by Hodgman 8 years, 2 months ago

Hi,

in various places around the internet I've seen strong recommendations against using exceptions in C++, for a variety of reasons (performance, semantics, code size, etc). Can anyone here make any comment on whether these arguments still hold today? I agree that exceptions aren't necessarily the best means of error handling, but in some cases they meet requirements that return-code style handling simply does not.

As game developers I'm sure many of you are biased towards disabling them completely (for the reasons above), but if that's your suggestion I'd appreciate it if you could supply hard evidence from updated compilers to support it.

Thanks

Advertisement
These days a lot of the concerns about using exceptions are no longer valid.

It used to be that enabling and using exceptions incurred a huge performance and memory hit.

These days the memory hit is tiny, and the performance implications not worth bothering about unless you're dealing in engine code within an AAA game where every cycle counts.

For the average indie game enable exceptions and use them where it is sensible. E.g. For exceptional circumstances as per their name.

Use an exception to indicate failure to initialise sound for example, but not failure to render an animation frame.

Be aware of exception safety for example when using constructors, destructors and new.

Have fun!

Yeah, I'll definitely fall into the camp you thought and disagree with braindigitalis here. :)

Exceptions are terribly problematic. This has far less to do with performance or overhead than you think. It has more to do with them being a feature that perverts basic control flow of an application in extremely difficult-to-predict ways. Writing exception-safe code is _immensely_ difficult, even with modern C++ features. There are very simplistic containers and algorithms that are literally impossible to make truly exception safe (as in offering the desired strong guarantee) as you can't ensure that all invariants will be maintained.

They're are a bad feature.. in _any_ language. They're bad in C# or Python or whatever, too. It's incredibly difficult to write correct code when you can't even trust the basic control flow of your code because it can be magically violated without clear annotation. It's not as bad as in C++ because of garbage collection (even with modern C++, move constructors/assignments can throw, because they might allocate, because the standards committee hates common sense or something), but they're still susceptible to container, algorithm, or object invariants being broken because of exceptions thrown at inconvenient times.

Making exception-safe code requires compromising your architecture (e.g. using the inefficient std::unordered_map instead of a sane closed hash table) and vastly complicating simple code.

The world has come a long way since exceptions were originally conceived; they're an unfortunate anachronism that are best brushed under the rug of history.

Sean Middleditch – Game Systems Engineer – Join my team!

Exception handling in C++ is more expensive than exception handling in some other languages where it is used frequently. There are costs to evaluating and unwinding the stack, cleaning up resources, and so on.

When it comes to standard conditions that are prone to happening it is generally less effort to handle it directly rather than through exceptions. Things happen. Files may not be there, rendering surfaces get lost, network connections time out. Prepare for it explicitly rather than using c++ exceptions.

Many places where C++ exceptions happen in games are either fatal conditions (like running out of memory) or conditions that could have been avoided through better practices (typecasting failure, improper use of a std::function or similar, out of range exception). This is in contrast to languages like Java where exceptions are more central to the language and have relatively less processing cost.

There are some penalties with using exceptions that you may not be aware of. Compilers generally won't inline functions with exception specifications. When you make a mistake with exception specifications the program will terminate(), instantly ending rather than gracefully handling the conditions. Templates generally have all kinds of problems with exception handling because the types are not easily known.

Making programs exception safe is very hard. Trying to keep your code at least exception neutral is difficult. Many of the most notable names in C++, like Bjarne Stroustrup and Herb Sutter, have written quite a few articles about it. Items need to remain invariant in the case of exceptions: if the exception happened in the middle of a sort, repairing the object's condition would mean unsorting to the original state; if the exception happened in the middle of assignment, repairing the object means it must be in the same state it was before the assignment operation started; if we allow for any object to throw in any operation a class containing multiple exception-throwing types cannot have strong exception-safe operations in even the most basic operations like assignment.

While exceptions can work out okay in languages like Java, and in some business code it might make sense to use C++ exceptions, extensive use of exceptions in C++ as used in games tends to introduce more problems than they are worth.

I do agree with what Sean said.

Exception safety is a complex beast, and these arguments remain.

It used to be that memory and performance were the main concerns of enabling exceptions as was the case with many c++ features we now use blindly without concern. In days of old inheritance was considered a memory and cpu intensive feature.

Do certainly be aware of the issues Sean raised, they are still valid and exceptions can be a minefield waiting to happen.

As I said use them sparingly and for exceptional circumstances. Don't fall into the java trap of having silly exceptions for any minor program condition! -- C++ is not java and is not C#! :)


The world has come a long way since exceptions were originally conceived; they're an unfortunate anachronism that are best brushed under the rug of history.

Except that STL and even core language features throw exceptions to communicate errors. There are many, many cases where exceptions are a massive PITA, but there are also cases where they work better than return values. Constructor errors are a problem, and requiring a return value to make room for an "error" value is not ideal. The new operator throwing by default instead of returning nullptr has always struck me as an incredibly dick move, but not nearly as dick as the syntax for non-throwing new..

If you plan to recover from any of this shit then writing exception safe code isn't really optional. That said, throwing and catching isn't really the problem here. Those are both pretty easy to do, and throwing is even downright convenient if you want to include an error message.

Fortunately, for game development recovery is usually not necessary. If you plan to write code for a mission critical server or something then you're stuck, but if you're just writing a game then most errors can CTD with an error message. That said, it would make sense to catch in main() and print e.what() or even just const char* in a dialogue, then throw descriptive error messages from wherever you want.

Of course, that's ignoring overhead, and in any case it might just be far more convenient to have a core language feature that could do the same job without the overhead or explicit catch.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

The error cases in the c++ standard library that throw exceptions can be avoided easily:

Out of memory is a bad problem if you encounter that exception, but you shouldn't if you are managing memory correctly. If you start getting this your program will be crashing in the near future.

Out of range exceptions and length error exceptions mean you screwed up, and you shouldn't have gotten into that situation. There isn't much excuse for getting these because the game controls the ranges and lengths of items.

Invalid argument exceptions, similarly shouldn't be encountered in practice.

Bad cast exceptions can be avoided by using a different type of cast. No need to invoke the more expensive exception version when a different version exists that returns null.

Etc., etc.

C++ exceptions with modern compilers are zero cost (or very close to it) when not thrown. Exceptions can even improve performance by avoiding lots of checks and cache invalidations that checked error codes can cause.

They are more expensive when thrown.

Exceptions are the only way to return errors from constructors. Exceptions cannot be silently ignored. Exceptions make error handling obvious and easier to separate from function logic.

They are not to be used for normal control flow. (3dsMax, I'm looking at you... whose bright idea was it to use C++ exceptions to implement "return" in your scripting language?)

The "basic exception safety" guarantee is trivial to provide, assuming you are following good C++ coding practices and RAII, and put a tiny bit of thought into your interfaces. The "strong" and "no throw" guarantees can be much harder (and sometimes impossible) to enforce, but that's ok, because 99% of the time you don't ever need to provide those. People who complain that certain algorithms and containers cannot be made "exception safe" don't always realize they can't be made error safe either for the exact same reasons. After all, exceptions are just automated error return values separated from the normal return value.

People also tend to forget that not catching an exception is also perfectly acceptable. If you can't handle the exception, don't. If an exception happened, you only need to handle exceptions if you can somehow still fulfill your contract in the face of the particular exception - otherwise let the C++ language and RAII handle cleanup and get out of the way so someone else can handle it.

On the flipside there are the following concerns: Most game engines and code are not designed to be exception safe and do not properly use RAII and similar C++ mechanisms and so adding exceptions to them becomes a minefield. It is non-trivial to figure out what exceptions a function call may throw, without good documentation (Java tries to avoid this by specifying exceptions in the function header, but that tends to make things even worse). This is especially true in C++ when operator overloading is in effect. It is also sometimes hard to figure out when throwing exceptions is the correct thing to do.

In short, there are upsides and downsides to exceptions, just like any other feature in any other language. They are not a silver bullet for error handling, but neither are error codes or asserts as others would have you believe. C++ supports multiple forms of reporting errors, all of which should be considered and evaluated.

Above all - take everything you read (even this post) with a grain of salt. If you are happy and used to using error codes, keep using them (but you may want to at least consider following basic exception safety coding styles as they help prevent other kinds of bugs like memory leaks and memory corruption). If you want to try exceptions, go for it! And, like everything else, use your profiler if you care about performance.

Additional information about exceptions from Stroustrup, the creator of C++.
On old game consoles, the recommendation (or default behaviour) was to disable exceptions. C++ is supposed to have the philosophy of "only" pay for what you use, but exceptions created a cost across your entire program regardless of whether you ever threw or not. On CPUs with small instruction caches, this was a big deal, so we killed them off. That cost is still present, but not as noticeable on modern CPU's as it used to be.

As for throw itself, on x86 and a lot of other architectures, it is really slow to throw an exception, so they should never be used as a normal (expected) flow-control mechanism.
On x86-64, throw can be implemented a lot better, where now it's similar in cost to a virtual function call.

At least one of the current generation of games consoles now recommends that you never disable exceptions, because they use them liberally in their SDK...

If you are writing game engine / middleware code, I would recommend making it compatible with compilers that have disabled exceptions (i.e. don't use them, or hide them with macros that can be disabled) as many of your customers may still want to disable exceptions in their compiler.

As for the actual language side of things... Writing truely exception-safe C++ code is hard. Really hard. As in, no matter what you're doing, you have to be thinking about exceptions in depth for every single line of code that you write. Most people don't want to pay that mental tax...

There's 4 levels of guarantee you can give when writing a function. For the sake of example lets say that we're writing a function that's called when a grenade explodes -- it spawns a bunch of effects and damages nearby objects. Half way thru the function, an exception is raised.
1) no guarantee. Who knows how it behaves if an exception is thrown. This is all code until you've gone over it with a fine tooth comb... In our example, maybe thr grenade object is left with invalid invariants such that next time a function is called on it, the game crashes.
2) no leaks. Proper use of RAII makes this fairly simple. If something throws, you'll clean up all your resources. Program state still may have been modified though. e.g. we've damaged half the nearby objects, but not the other half. Ifthe caller tries again, those first half of the objects will receive double explosion damage. At least we wont crash though.
3) The strong guarantee -- aka transactional commit/rollback. The function either succeeds completely (returning without exception), or it makes no changes to the program state whatsoever. This is the really hard on in practice. E.g. If we're half way through our "do damage" loop when an exception is raised, we need to reverse back through that loop and re-add all the health points that we have remove so far. This gets harder as exceptions are thrown longer distances -- if a particle effect throws an out-of-memory, which is caught in the main game loop, then you need a mechanism to perform a roll-back of that entire frame!
4) the no-throw guarantee. No matter what, your function can never be involved in exceptional stack unwinding. This means that all the functions that you call must also provide this same guarantee (or they'll) infect your function. This one is hard to achieve in practice because there's no way to know if a 3rd party function may throw or not. Simply disabling exceptions at the compiler level magically bestows this guarantee on your entire code-base though :)


If exceptions are enabled, you should aim for level 3/4 guarantees. This will have a major architectural impact on your game. Most games are not built using a transactional model that supports easy roll-backs, making level 3 very painful to achieve.
This classification is also completely informal and not verified by the compiler - so you may think you're giving a level 3 guarantee, but are actually giving level 1 due to a subtle bug in your code.

C++ is much easier to read/write/maintain if we all just pretend that exceptions don't exist.

4) the no-throw guarantee. No matter what, your function can never be involved in exceptional stack unwinding.


I would instead put it as "this function will never throw an exception". A little clearer wording.

If exceptions are enabled, you should aim for level 3/4 guarantees.


Why? There is nothing wrong with the "basic guarantee" that says "nothing is leaked and I am in a valid, if unspecified, state".

As yourself and others have already stated, trying to provide the "strong" or "no-throw" guarantee is very hard, sometimes impossible, and usually has large penalties.

But there is little to no reason to go for it in a game. Heck, the standard library itself only provides "strong" or "no-throw" when it can do so for little to no cost, and guarantees to provide the "basic" level of exception safety across the board.

This topic is closed to new replies.

Advertisement