is Exception Handling widely used in commercial games programming?

Started by
130 comments, last by swiftcoder 18 years, 11 months ago
Don't use exceptions for things that are likely to happen sometime, like a missing texture. At least not in production code.

But I would advise to do use them as much as possible in Debug builds. After all, even a missing texture should be fixed when encountered. Let the game testers work with the Debug build for a long while. Try, as good as possible, to ensure you always have a fallback for Release mode. For the example of texture loading, return null and deal with that gracefully in the rest of the code. Test that!

You might want to use something like the code below in Release builds:
#define try#define throw {log(__FUNC_NAME__);} if(false)#define catch if(false)

So the general idea is that you should use exceptions as a better alternative to asserts. They're use in Debug builds and meant for code testing.
Advertisement
Quote:Original post by C0D1F1ED
Don't use exceptions for things that are likely to happen sometime, [...]

So the general idea is that you should use exceptions as a better alternative to asserts. They're use in Debug builds and meant for code testing.


No.

The one and only point of exceptions is, as their name implies, to identify and handle exceptional situations. If you throw an exception, you imply that:

- What happened is normal and expected program behavior under that situation (and that the exception-throwing behavior is fully documented)

- The situation can be restored to a stable state (and the exception catching mechanism will help you do it)

An assertion failure meets neither of these two conditions: it is meant to signal unexpected behavior (situations that should not occur), and usually an assertion is not recoverable (if only because to recover, you'd need to actually know why the unexpected behaviour occurs). Using exceptions only as an assertion mechanism is a gross misuse.

Quote:
For the example of texture loading, return null and deal with that gracefully in the rest of the code.


This is an interestingly inefficient thing to do. Notice that most of the code you write to "deal gracefully" with an error will consist in:

- Detecting an error has occured in a given function you just called
- Determining the error, and if you can handle it right there
- Handling it if possible
- Propagating it by returning immediately an error code

Which is also casually referred to as "exceptions". Only you do it yourself.

This set of operations, when written by hand, is often underoptimized (for instance, it will require a call to GetLastError( ) because there's only so much data you can get from a NULL return value) and slower than exceptions (unless, of course, you claim to spend time optimizing your error recovery code, at which point I'll just roll my eyes and go away).

Not only that, but it is harder to maintain since you actually have to propagate the error by hand (on the contrary, an exception propagates itself automatically until it is caught). When having to do this on a scale of hundreds of possible error-generating calls, it is usually enough to drive someone crazy (or, more often, to end up in code that does not handle exceptional situations well). It is also less readable, since the error-checking code is to be found around the function calls (as opposed to catch blocks that occur at the end of the entire try block) and as a consequence the code for normal situations and exceptional (sic) situations is mixed together, making the program logic slightly harder to follow even when commented. Besides, it requires a hired programmer to be trained in your own error-handling system (whereas any decent programmer knows how try { } catch { } blocks work).

Exceptions are a much more elegant, fast, reliable, maintainable, universal and easy way to implement "graceful dealing" with errors on a scale larger than 100-line programs, and when available they should be used, as they are an option superior to any other complete method of handling exceptional situations, in every possible way.

Of course, if you go the incomplete error handling way (such as giving up when an error happens instead of trying to recover) or only need error checking in a 100-line function (not realistic, but still, who knows?) then exceptions are not really useful.
Because some people have no idea how exceptions should be used, I'll give you my best example of a good-to-use-exceptions-in situation here:


try{    ASerializeReader ser(filename, SE_LEVEL_FILE_ID);    ASharedPtr<SEPlayField> pfnew(new SEPlayField);    pfnew->Serialize(&ser);    // if we havn't thrown an error by now, we have loaded successfully    pf = pfnew; // set to new playfield}catch(ASerializeError e){    // give the user an error ("failed to load level")}


The above it the code used for loading a level. Note the use of a smart pointer that will clear the new play field in the event that it goes out of scope without increasing its reference count by assigning itself to the "active" playfield (pf).

Already, by using exceptions and a smart pointer, it's very safe and very easy. There are no memory leaks ever, and we can report the error and maintain control over our application.


Now the reason that this requires exception handling is that the serialization functions (that actually do the data flattening/unflattening) look something like this:

void Example::Serialize(ASerializer* s){	s->Data(&foo);	bar->Serialize(s);}

Now, because we're reading a file, a huge number of things could go wrong. The call to Data(), for instance, could fail if it falls off the end of the file (file is short, perhaps due to bad download). We could also throw for other reasons - bad version number, bad checksums, missing classes, etc. And of course the child object, bar, could also have an error.

Now, if I had to do that without exceptions, the code for that simple serialization function would look something like the following. And remember this is a very simplified function - these things could fail in a mirrad of ways, in every single place.

bool Example::Serialize(ASerializer* s){	if(!s->Data(&foo)) return false;	if(!bar->Serialize(s)) return false;	return true;}

As you can see, it's already way more code than we started with. However you may note that I catch ASerializeError, which gives me information about what actually failed (which may be useful to the user - particularly mod authors). The non-exception code would then have to look something like this:

ASerializeError Example::Serialize(ASerializer* s){	ASerializeError e = ASerializeError::NoError();	e = s->Data(&foo);      if(e->IsError()) return e;	e = bar->Serialize(s);  if(e->IsError()) return e;	return e;}


Notice we're introducing a lot of code repetition. At least now we're giving off an error message that is actually useful to the user (like "your file is too small", "your file is corrupt", "your file is too old").

Now this is the "10-fold increase in code" case that I mentioned in my first post. Although this example isn't a 10-fold increase, the actual usage would be. Notice that the only code that we have actually added in that increase is just checking if there is an error - which is 100% repeated code.

And of course, if something fails to check that error condition. Then you get a fatal error that will bring down the program with a crash.


The good thing about exceptions is that different types allow you to deal with and give off different errors in different areas. For example, you might want a "out of memory" error to just cause the whole app to terminate, and perhaps give the user a message box to that effect (there isn't a lot that can be done without memory anyway). Whereas, you'd want file ("I can't load this") exception to both come from a different place (whatever is directly handling the file), and be handled in a different place (an error for the user, failture to load the level, not an application termination).


Of course, it's easy for me, as I'm the only programmer. As some of you say - in a team, or using a library - you have to deal with the exception safety of other peoples code. Frankly that's a load of crap. The code should be well documented so everyone knows what exceptions could be sent and recieved from where and when and why. If your programmers don't know how to make exception-happy code, then they are not qualified C++ programmers.

Even then - there are things that don't even need to care about exceptions - especially as the exceptions (unlike error codes) can pass right through them without any change in code.


Anyway, my example is an extreme case - where there is no practical way of doing things without exceptions. The same idea applies in many other cases too. And while I would avoid using exceptions for "normal" logic flow (things that will happen regularly) especially in "realtime" code, they're very useful for handling exceptional cases in many things - reading and writing files, transmitting data over sockets, users unplugging joysticks, etc, etc. It dosn't have to be an error case, either - generally they should be used for "this particular line of program logic dosn't work, try another" - which includes more than just errors.


I really don't want to see code where the only try/catch block is this:
int main() { try {entire_program();} catch(...) {print("death");} }



Also, many people are saying how exceptions lose stack information. Lies. MSVC 7.1 allows you to automatically break-on-throw in its debugger. If your debugger can't - that is not a fault of C++.

The only limitation of is that you lose stack information for release code. This is what you see when there is an error in the Unreal Engine (I don't have an example handy). This isn't usually necessary for most programs, the reason the Unreal Engine really "needs" this functionality, is because of UnrealScript. Most other programs can get away with a well written throw-type with plenty of verbose information that can be given to the user. My ASerializeError class, for example, has a function AddSerializerInfo(const ASerializer& s) that adds information about what file was being read, what version, etc, etc - all the info a user would need (user includes mod author).

And Unreal manages to implement the stack-listing functionality using some macros anyway.


Anyway, I hope this was informative. Use exceptions responsibly people [smile].
'Fraid I don't have all that much to add here, having not much in the way of experience with large projects (I just hit 4000 lines of code... That is to say, nothing..) or extreme error handling (Sounds like it could be a sport, doesn't it? Extreme Error Handling? [smile]). But I'll give my two cents anyway..

Basically, I would (well, plan to, I haven't implemented this yet) use exceptions whenever problems out of my control occur, such as a disk read error, network error, whatever, as well as in places where the error can not simply be shrugged off/fixed immmediately, such as if a texture could not be found.. Let's take that example. I would throw an exception with a point to the texture object that was supposed to be filled, and in the catch block, display an all-purpose MessageBox (not a Windows one, but a specialized object) and fill the texture object with the default 'error texture'.

Maybe this could be done by simply putting all that code in the catch block where the error occurs.. However, let's imagine I then have a number of different texture-loading related methods, such as LoadAlphaMap and LoadNormalMap, or even LoadDisplacementMap. Now what? Do I paste the code where the error can occur in all those functions as well? This leads to code bloat, as well as unmaintainability. Am I then supposed to remember to update all four sets of error handling code when I want to change it? This can be handled much better with exceptions, I would think.

Oh ya, one question.. A lot of people have been mentioning the Unreal source code, could someone perhaps point out to me how I could get my hands on this, as I am most interested in it [smile]
Free speech for the living, dead men tell no tales,Your laughing finger will never point again...Omerta!Sing for me now!
Andrew Russell, I don't know where in the hell you learned to program, but you should consider revising your code instead of immediately calling it 'bloated'.

//your 'bloated' codebool Example::Serialize(ASerializer* s){	if(!s->Data(&foo)) return false;	if(!bar->Serialize(s)) return false;	return true;}//the same code, written *better*bool Example::Serialize(ASerializer* s){  return (s->Data(&foo) && bar->Serialize(s));}


Ah - but - two things:

First: that was an over-simplified version, the "actual" use is far more complicated. It was layed out like that for simplicity. It can also contain jumps (if, while, etc) which rules out using a single statement for it at all.

Second: It fits in well when you add in the ASerializeError error type in the next example.


I am aware of your reduction in number of lines. The first time I wrote the example code using ASerializeError it was more succinct, but it was harder to interpret. Particularly because it uses the fact that ASerializeError has a conversion operator to bool. It looked something like:

ASerializeError Example::Serialize(ASerializer* s){    ASerializeError e;    if(e = s->Data(&foo)) return e;    if(e = bar->Serialize(s)) return e;    return e;}
That still lets me do advanced flow-control things. If I wanted to do it your way, it would have looked something like:


ASerializeError Example::Serialize(ASerializer* s){    ASerializeError e;    if(e = s->Data(&foo) && e = bar->Serialize(s)) return e;    return e;}// or even:ASerializeError Example::Serialize(ASerializer* s){    ASerializeError e;    if(e = s->Data(&foo))        e = bar->Serialize(s);    return e;}
The entire idea of the code in the first place, both in my examples and in my actual production code, is to be more readable and eaiser to use and modify.

Not to make it take up less lines, and possibly make it a few percent faster if the compiler misses an optimisation. This is loading code anyway - it hardly needs to be realtime.


Not to mention - I know what I'm doing - I don't use any of those methods - I use exceptions for the most readable, succinct and efficient code possible.


In conclusion - calling me out for my "bad" code was totally unnecessary. I hope, in your eagerness to do so, you havn't missed the point.
I haven't missed the point, but when you don't use exceptions (like I do), you tend to try to cut the code down to the simplest form, while keeping it working (exception prevention, instead of exception handling). Exceptions have their place, but you don't *need* to use them.
Well, the entire point of my post is where you do need exceptions, at least to write sensible code. The examples I provided were not entirely sensible (which was the idea). Neither was your code - and it was much, much less readable (and would not have worked in the "real" case either).

The general case of my specific example is any set of large, recursive calls that operate on tree structures, where the leaf calls of that tree can generate... well... exceptions, which in turn cause the entire tree to be invalidated.

Without exceptions, this would require huge ammounts of return-value checking. Not to mention, having to manually delete objects that are created on the heap.

Hence you missed the point.
Quote:Original post by Gorax
I haven't missed the point, but when you don't use exceptions (like I do), you tend to try to cut the code down to the simplest form, while keeping it working (exception prevention, instead of exception handling).

I find this extremely hard to believe and it is contrary to my experience with code I have written or used, regardless of scale or complexity.
Quote:Exceptions have their place, but you don't *need* to use them.

Of course you don't need to use them. You don't need to use a medium or high level language. You could write everything in machine code or on a Turing machine. The point is that exceptions make it easier to write simple, correct, grokable code. Use them.

Enigma

(I think I just used up my quota of <b> tags for the day [lol])

Quote:Original post by Gorax
I haven't missed the point, but when you don't use exceptions (like I do), you tend to try to cut the code down to the simplest form, while keeping it working (exception prevention, instead of exception handling).


surely by cutting the code to the simplest you've lost readability? and until something is judged to be a bottleneck readability > speed considerations.

I find the charge of 'exceptions being slow' an intresting on, generally when an exception accures you've fallen off the happy fast path and have crashed into a bumpy field of errors and problems which have to be dealt with, at which point I'd consider speed the least of my problems.
That said, if you use exceptions for every "error" condition you deserve any runtime pain you recieve [grin]

This topic is closed to new replies.

Advertisement