Skip to main content
GameDev.net gamedev.net
🔒 Locked

[C++] exceptions vs error codes

Started by Daggerbot Dec 10, 2008 at 1:59 PM 51 replies 8.1k views
Original Post
Daggerbot
Daggerbot
Do you generally prefer to use exceptions or error codes in your games/apps? And why? For example, if I have a malloc wrapper...
static void* MyAlloc( uint size )
{
  register void* result = ::malloc( size );

  if( result == nullptr )
    throw ExAllocation();
}

...vs...
static void* MyAlloc( uint size )
{
  return ::malloc( size );
}

In the first version, an exception will be thrown if malloc failed. I would probably not handle this exception, as I would make tons of calls to this function, so the program would crash, and I could get a stack trace from the debugger. In the second version, the program would run until it crashes due to an access violation. The location of the error would be harder to trace, but if malloc fails, I'm probably doing something completely wrong in the first place. And another case:
template< typename I, typename C >
static I ConvertStringToInt( const C* string )
{
  if successful
    return result;
  else
    throw ExConversion( Integer::GetProperties()->ToString() );
}

...vs...
template< typename I, typename C >
static bool ConvertStringToInt( const C* string, I* out )
{
  if successful
  {
    *out = result;
    return true;
  }
  else
    return false;
}

In this example, it would work just fine either way, as long as the ExConversion is handled when using the first. These may not be the best examples. What I would like to know is when exceptions are the absolute best way to handle errors. If they can easily be avoided with little cost, I may not use them at all in my current project. Any help is appreciated. EDIT: This is hypothetical code. I don't actually use malloc in C++ code. [Edited by - Daggerbot on December 11, 2008 8:17:12 PM]
Antheus
Antheus
Quote:
Original post by Daggerbot

What I would like to know is when exceptions are the absolute best way to handle errors.


Exactly when they are the absolutely best way to handle errors. In all other cases, they are not best way to handle errors.

From here, religious debates may continue as to which are better, but in for purposes of engineering, requirements are analyzed, and solution is provided that matches those requirements. There is no silver bullet, there is no best way, there is no final answer.

For example, exceptions are not the best way to handle errors if one is using compiler with exception support disabled.

Quote:
ConvertStringToInt


If this operation fails, is it catastrophic? Is it important? Does the result matter?
int convertToInt(string) {  if (failed) throw Exception();}bool convertToInt(string, out int) {  if (failed) return false;}int convertToInt(string, int defaultValue) {  if (failed) return defaultValue;  ...}
The above 3 methods are equivalent in functionality, but intended for different uses.

How catastrophic is failure to convert an int? Consider this:
while (!eof) {  convertToInt(readLine());}
When reading a file, the above conversion fails.
- Can you continue reading? If not, throw an exception.
- Can you ignore the value, or try to repair it? If yes, use the second version
- Can you simply ignore the value, and use anything? If yes, use the third version.

Version 1 (we can't recover, so we explode) would be:
while (!eof) {  foo(convertToInt(readString));}


Version 2 (we might be able to use some of it) would be:
while (!eof) {  s = readString;  if (convertToInt(s, val)) {    foo(val);  } else {    foo(tryToRepairBrokenString(s));  }}


Version 3 (who cares, user had their chance to set the values, we need to move on regardless) would be:
while (!eof) {  foo(convertToInt(readString, 0));};



The answer to all your questions is: it depends.


PS: what are malloc and register keyword doing in C++ code?
Zahlman
Zahlman
Quote:
Original post by Daggerbot
Do you generally prefer to use exceptions or error codes in your games/apps? And why? For example, if I have a malloc wrapper...
*** Source Snippet Removed ***
...vs...
*** Source Snippet Removed ***
In the first version, an exception will be thrown if malloc failed. I would probably not handle this exception, as I would make tons of calls to this function, so the program would crash, and I could get a stack trace from the debugger.


I cannot think of a combination of task, hardware, platform, etc. that could possibly make it sane to use C++ exceptions but also use malloc() in preference to new. In any case, new throws exceptions by default, so the decision is pretty much made for you in C++.

Of course, there is a much more general design question revolving around error codes vs. exceptions, but generally you should prefer the exception approach whenever the cases that would trigger an exception are actually... exceptional.
phresnel
phresnel
I have it a bit inconsistent in my current project, and haven't decided yet which way when to go. But let me add some notes:


  • using exceptions forbids some optimisation options in g++ (but the compiler will warn you about this; generally, make -Wall one of your mandatory options when you use g++)
  • there's an attribute in gcc to tell it to check if every return-value is checked/veryfied by the caller (see here)


As you see, while it's not The One Clean Way Of OOP, using no exceptions can be usefull in c++ for some specific purposes.
Jan Wassenberg
Jan Wassenberg
Quote:
I cannot think of a combination of task, hardware, platform, etc. that could possibly make it sane to use C++ exceptions but also use malloc() in preference to new.

Well then let's get the list started:
- allocating storage for use during/after an exception has been caught (<- possibly a bad idea because your heap may be hosed, but still)
- allocating aligned storage for non-POD types
- placement new for any other reason

Quote:
you should prefer the exception approach whenever the cases that would trigger an exception are actually... exceptional

That sounds reasonable but is in fact pretty poor advice because it's so weakly defined. It's like answering the question of how much salt to use when cooking with "just a dash" - probably sufficient for those who already know what is meant but utterly unhelpful to those who do not.

So: What is 'exceptional' - does it require a low probability of occurrence (and how is this threshold to be set?), or should such cases 'never' happen during 'normal' operation (but what then is 'normal'?), or does the incidence of such an event imply uncontinuable doom for the program? To me the latter definition sounds reasonable and is fairly well-defined, but that's probably more restricted than you'd like. Anyway, the point is: it is worth thinking about this (and passing on advice) in much more detail.
E8 17 00 42 CE DC D2 DC E4 EA C4 40 CA DA C2 D8 CC 40 CA D0 E8 40E0 CA CA 96 5B B0 16 50 D7 D4 02 B2 02 86 E2 CD 21 58 48 79 F2 C3
SiCrane
SiCrane
My general rule of thumb is if out of every 10,000 times the function is executed do you expect at least one error? If so, then use a return value; otherwise, use an exception.
Cornstalks
Cornstalks
Quote:
Original post by SiCrane
My general rule of thumb is if out of every 10,000 times the function is executed do you expect at least one error? If so, then use a return value; otherwise, use an exception.


So for things like connecting to a server or something web related you would use a return value since it is very easy for something to go wrong? But for something like parsing an xml file you'd use exceptions if there was an error? I'm just trying to better understand what you consider return-code worthy and exception worthy.
SiCrane
SiCrane
I would regard a bad XML file to be more likely than 1 out of 10,000 times. Especially if it's a configuration file that a human being may have edited. Pretty much any situation that involves I/O or validating input would be return code situations. This is actually an area where I would regard things as failing more often than it would in the past. While the actual hardware has gotten more reliable, from my experience it seems much more common to run things off of a flash drive or external hard drive, and those things come loose if you look at them funny. Or may not be present if the user put their laptop to sleep and woke it up without their USB hub plugged in.
quasar3d
quasar3d
The whole games industry, except for me, seems to agree that return codes are the way to go. Microsoft didn't even bother to implement exceptions decently for the xbox 360 compiler.

But personally, I think exceptions are infinitely better, as exceptions fit so well in the concept that objects should be constructed in their constructor, and are either fully completed when the constructor is done, or just not there at all. Using error codes just seems to promote the sloppy code style with tons of uninitialized data, null pointers that don't make sense all over the place, which results in "fixes" that just put an if statement around the place where it crashes, which more often than not breaks something else...

This all wouldn't be a problem if people would just start thinking of their objects as actual objects, and not containers of data, that may be there, but may just as well be not there. And exceptions align so well with that concept, while error codes only promote fragile spaghetti code.

sry for the little rant, I'm just frustrated by some code I'm currently debugging:)
Cornstalks
Cornstalks
Quote:
Original post by SiCrane
I would regard a bad XML file to be more likely than 1 out of 10,000 times. Especially if it's a configuration file that a human being may have edited. Pretty much any situation that involves I/O or validating input would be return code situations. This is actually an area where I would regard things as failing more often than it would in the past. While the actual hardware has gotten more reliable, from my experience it seems much more common to run things off of a flash drive or external hard drive, and those things come loose if you look at them funny. Or may not be present if the user put their laptop to sleep and woke it up without their USB hub plugged in.


Ah I see. I was thinking XML files would be less error prone just because of their strict standard, but I didn't think about the fact that a human edited XML file is indeed very error prone. Bloody typos.
Antheus
Antheus
Quote:
Original post by Jan Wassenberg
So: What is 'exceptional' - does it require a low probability of occurrence (and how is this threshold to be set?), or should such cases 'never' happen during 'normal' operation (but what then is 'normal'?), or does the incidence of such an event imply uncontinuable doom for the program?


The only general purpose criteria I can think off is atomicity.

Exceptions are naturally suited for operations that should resemble atomic constructs. Not in database transaction sense. Some operations don't need that guarantee.

An example of such approach is what I've used for compression. All compression schemes can result in output being larger than input. Since I don't want to compress something and result in more data, Compressor returns either success code or boffer_too_small error, indicating compressed data would be larger than input (I pass input buffer size as output buffer size). All other errors however are thrown as exceptions - I can't handle them, so I let caller discard entire operation (atomicity).

Basic approach:
int compress(in, out, size);void bar() {  int packed = compress(in, out, size);  if (packed > 0) {    use(out, packed)  } else {    use(in, size)  }}...try {  foo();  bar();  baz();} catch () {  // fatal}
The above represents best of both worlds. Reasoning behind this is quite simple. State that I have control over can optionally be handled by error codes. I know what compress() does and what I need it to do. But if I run out of memory, something went wrong in state I have no control over.

So foo() bar() and baz() are expected to succeed atomically, or not at all. What isn't shown above is that I prefer to keep state local if possible. So all 3 statements can be considered atomic transaction. All 3 succeed, or none at all.
Daggerbot
Daggerbot
Quote:
The whole games industry, except for me, seems to agree that return codes are the way to go. Microsoft didn't even bother to implement exceptions decently for the xbox 360 compiler.

But personally, I think exceptions are infinitely better, as exceptions fit so well in the concept that objects should be constructed in their constructor, and are either fully completed when the constructor is done, or just not there at all. Using error codes just seems to promote the sloppy code style with tons of uninitialized data, null pointers that don't make sense all over the place, which results in "fixes" that just put an if statement around the place where it crashes, which more often than not breaks something else...

This all wouldn't be a problem if people would just start thinking of their objects as actual objects, and not containers of data, that may be there, but may just as well be not there. And exceptions align so well with that concept, while error codes only promote fragile spaghetti code.

sry for the little rant, I'm just frustrated by some code I'm currently debugging:)

I agree with this for the most part, but what really is the difference between these two pieces of code?
FileStream* st;try{  st = new FileStream( "applesauce.x" );}catch( std::exception& ex ){  app->KillSelf();}

FileStream* st;if(( st = fs->Open( "applesauce.x" )) == nullptr )  app->KillSelf();

I can think of at least one thing that is for sure better about exceptions. An unhandled exception will crash the program immediately, whereas unhandled error codes could cause a delayed crash and take a little longer to debug. So far in my project, I've been using mostly error codes, but I use exceptions when it's completely necessary that the application stops running before something bad happens, for example if it would otherwise write a corrupt file.
Daggerbot
Daggerbot
Another question. Does anybody vomit at the sight of this hypothetical code?

try{  // check header  if( bih.biSize != sizeof(BITMAPINFOHEADER) ) throw 0;  if( bih.biWidth <= 0 ) throw 0;  if( bih.biHeight <= 0 ) throw 0;  if( bih.biPlanes != 1 ) throw 0;  if( !supportedBitCounts.Contains( bih.biBitCount )) throw 0;  if( !supportedCompression.Contains( bih.biCompression )) throw 0;}catch(...){  MiserableFailure();}


EDIT: I almost feel like a goto statement would be the best solution here, as opposed to disgusting use of exception handling, or a million calls to MiserableFailure.
Sneftel
Sneftel
Quote:
Original post by Daggerbot
Another question. Does anybody vomit at the sight of this hypothetical code?

*** Source Snippet Removed ***

EDIT: I almost feel like a goto statement would be the best solution here, as opposed to disgusting use of exception handling, or a million calls to MiserableFailure.

Uh... what's wrong with boolean operators?

if( bih.biSize != sizeof(BITMAPINFOHEADER) ||    bih.biWidth <= 0 ||    bih.biHeight <= 0 ||    bih.biPlanes != 1 ||    !supportedBitCounts.Contains( bih.biBitCount ) ||    !supportedCompression.Contains( bih.biCompression )){  MiserableFailure();}

Sneftel
Sneftel
Quote:
Original post by Daggerbot
what really is the difference between these two pieces of code?

The first is idiomatic for the function. The second is idiomatic for the language. Decide how many idioms you feel like remembering. (Of course, there are far more reasons to use exception handling... they just aren't well demonstrated by such a trivial code snippet.)
quasar3d
quasar3d
Quote:
Original post by Daggerbot
Another question. Does anybody vomit at the sight of this hypothetical code?

*** Source Snippet Removed ***

EDIT: I almost feel like a goto statement would be the best solution here, as opposed to disgusting use of exception handling, or a million calls to MiserableFailure.


I really don't think there's anything wrong with that. In many cases, it would probably be better to do the MiserableFailure stuff inside a destructor of whatever object that was used, but I really don't think there's anything wrong with this.
SiCrane
SiCrane
Quote:
Original post by Daggerbot
Another question. Does anybody vomit at the sight of this hypothetical code?

This is a textbook example of how not to use exceptions. Exceptions should not be used for program flow.
Zahlman
Zahlman
Quote:
Original post by Jan Wassenberg
Quote:
I cannot think of a combination of task, hardware, platform, etc. that could possibly make it sane to use C++ exceptions but also use malloc() in preference to new.

Well then let's get the list started:
- allocating storage for use during/after an exception has been caught (<- possibly a bad idea because your heap may be hosed, but still)


Not sure what you hope to accomplish with this? (It's worth noting that solid handling of exceptions in C++ can be quite tricky. Especially if you're catching std::bad_alloc.)

Quote:
- allocating aligned storage for non-POD types
- placement new for any other reason


I don't tend to think of these tasks (when done explicitly, as opposed to using placement new implicitly via std::vector) as particularly sane, but yes, there are those who must do them.

Quote:
Quote:
you should prefer the exception approach whenever the cases that would trigger an exception are actually... exceptional

That sounds reasonable but is in fact pretty poor advice because it's so weakly defined. It's like answering the question of how much salt to use when cooking with "just a dash" - probably sufficient for those who already know what is meant but utterly unhelpful to those who do not.


Unfortunately, programming is not the rigidly defined science that people sometimes make it out to be, and skilled programmers do have to make these decisions and learn from experience.

But do try using exceptions. (Another option, when the natural return value is an object, is to return it by pointer and use a null pointer to indicate an error. This only works if you don't care what went wrong, of course; unless you want to get even hairier by using something like errno. Also, some might insist on using something more sophisticated, such as boost::optional.)

Quote:
So: What is 'exceptional' - does it require a low probability of occurrence (and how is this threshold to be set?), or should such cases 'never' happen during 'normal' operation (but what then is 'normal'?), or does the incidence of such an event imply uncontinuable doom for the program? To me the latter definition sounds reasonable and is fairly well-defined, but that's probably more restricted than you'd like.


Indeed. I wouldn't say unrecoverability is a requirement for using exceptions. Those situations are certainly strong candidates, though. It works well to throw the exception and then simply not catch it, in order to end the program; you'll still unwind the stack, and exit via std::terminate() which is usually defined to give some useful information.

Actually, it seems to me that one of the initial "big deals" about C++ - before everyone caught on to RAII - was that you could ignore an exception from new to end the program, or catch it to try something else. In C, you can wrap malloc() in a function that calls exit() if the return value is null, but it's not built in, and you don't get things (like, say, file handles) cleaned up for you - at least not without some atexit() hackery. It also means that you call a different function for the handle-an-error versus let-it-crash cases, instead of just adding error-handling code.

Anyway.

If the program has a hard-coded config file name and the config file is not there, that should probably throw an exception.

If it prompts the user for a file name and the named file isn't there, that's probably not "exceptional". Unless the user's file name choice comes from an operating system file chooser dialog.

Quote:
Anyway, the point is: it is worth thinking about this (and passing on advice) in much more detail.


On this I will agree 100% :)
SiCrane
SiCrane
Quote:
Original post by Zahlman
Indeed. I wouldn't say unrecoverability is a requirement for using exceptions. Those situations are certainly strong candidates, though. It works well to throw the exception and then simply not catch it, in order to end the program; you'll still unwind the stack, and exit via std::terminate() which is usually defined to give some useful information.


Whether or not the stack is unwound for an unhandled exception is implementation defined. If you want to guarantee that the stack is unwound, you'll need to handle the exception.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.