Jump to content

  • Log In with Google      Sign In   
  • Create Account


Portable Use And Disabling Of Exceptions


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
33 replies to this topic

#21 ApochPiQ   Moderators   -  Reputation: 14890

Like
0Likes
Like

Posted 20 February 2013 - 11:43 PM

Constructors are a weird situation in C++, but personally, unless failing to construct an object is somehow recoverable, I'd rather assert than throw. A fatal construction error is still going to fall into one of two camps: I can keep chugging along, or I need to terminate. In the latter case, asserts let you dump core on-site instead of unwinding a level and possibly losing context as to what happened in the constructor.

 

If you have genuinely recoverable construction errors, exceptions are pretty much your only option in C++, aside from redesigning your API to some extent.



Sponsor:

#22 Ectara   Crossbones+   -  Reputation: 2911

Like
0Likes
Like

Posted 20 February 2013 - 11:53 PM

I see. There aren't many instances where a constructor's failing is still recoverable, in my mind; a file stream failing to open a file is an example I can think of. One would need to check if the file was opened after the constructor returns; I find this acceptable. Opening a file has pretty much always been a two-stage affair: try to open it, then check if it is open.

I guess there's nothing to it but to try to design with one or the other, and see if I run into any problems where I'd need a more elegant solution. I'll start with trying to implement everything unexpected as assertions, and see where that gets me.

As a quick question, for my UTF-8 string example, if I assign invalid text to it, and it fails validation, is it overkill for that to be a cause for terminating the program?



#23 Cornstalks   Crossbones+   -  Reputation: 6974

Like
0Likes
Like

Posted 21 February 2013 - 12:13 AM

I largely agree with ApochPiQ. Here's my (simplified) take on assert vs. exceptions:
 
Assert is for when the programmer has screwed up.
 
Exceptions are for when the user (or the user's computer, but not the programmer) has screwed up.
 
Of course, you can replace exceptions with return codes. And, for the most part, I honestly don't find a lot of situations in which exceptions are truly necessary. They're minor details in a much larger picture, overall, because they're... exceptions (that is, they're not the rule).
 
Part of this question though is about when you do your sanity checking. Should you do sanity checking before accessing a vector element to make sure you're in bounds, or should you just go ahead and attempt the access and require the vector to do the sanity checking for you? If you design your library such that you require sane inputs, you can use mostly asserts instead of exceptions (or return codes). However, if you want your library to handle insane/invalid inputs without crashing, then you need to use exceptions (or return codes). In one case, the programmer violated your contract and your assertion catches this. In the second case, the programmer did not violate your contract, but there was still some type of program that requires either an exception or a return code. You need to decide where to draw the line. You can't put all the responsibility on the user of the library (after all, you need to at least handle failed allocations, for example), but you can put a good amount on the user of the library. For the erroneous situations you have to prepare for, like failed allocations, exactly how you handle it is up to you...
 
One option is to provide a custom error handler and a callback system. For example, you can do:
 
// set this userCallback to their own function. It's then their responsibility to make sure that if control returns// to your code, that the program isn't in an invalid state.void (*userCallback)(int, const char*) = die; void throwOrCallback(int errorCode, const char* errorMessage){#if USE_EXCEPTIONS    throw std _linenums:1'>void die(int errorCode, const char* errorMessage)
{
    // log if you want
    abort();
}
 
// If the user doesn't want to abort() on an error, but would rather handle it themselves, you can let them
// set this userCallback to their own function. It's then their responsibility to make sure that if control returns
// to your code, that the program isn't in an invalid state.
void (*userCallback)(int, const char*) = die;
 
void throwOrCallback(int errorCode, const char* errorMessage)
{
#if USE_EXCEPTIONS
    throw std::exception(); // or whatever, you can throw the right kind of exception based on the error code
#else
    userCallback(errorCode, errorMessage);
#endif
}
 
// Then, in your code, use throwOrCallback() instead of an actual throw statement

 
Another option:
#if USE_EXCEPTIONS#define ON_ERROR(errorCode, errorMessage) throw std _linenums:1'>
// I'm not a fan of macros, and this is semi-evil, but then again you're already doing some semi-evil stuff...
#if USE_EXCEPTIONS
#define ON_ERROR(errorCode, errorMessage) throw std::exception() // ... or whatever, you can have a fancy function to determine the right exception type
#else
#define ON_ERROR(errorCode, errorMessage) return errorCode
#endif
 
// Then, all of your library functions return an error code and use ON_ERROR for an error. For example:
int doStuff()
{
    if (someCondition)
        ON_ERROR(42, "You gave me the wrong answer. The right answer is 42.");
 
    return SUCCESS_CODE_OR_WHATEVER; // functions that can't fail don't have to return an error code
}

 
Basically, you have to just choose either exceptions or error codes. Or, if you want to use both (and let the developer decide between the two), you have to do something along the lines of the above.
 
Edit: WTF is up with GameDev.net and mangling my code?

Edited by Cornstalks, 21 February 2013 - 12:22 AM.

[ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

#24 Cornstalks   Crossbones+   -  Reputation: 6974

Like
0Likes
Like

Posted 21 February 2013 - 12:28 AM

I'm giving up on trying to fix the above mangled post. Gosh I hate the code mangling that always happens on this site.

 

As a quick question, for my UTF-8 string example, if I assign invalid text to it, and it fails validation, is it overkill for that to be a cause for terminating the program?

I depends on what your contract is. It's perfectly fine for you to have a strict contract that says "I expect only valid UTF-8, and if you give me invalid UTF-8 I will assert() your sorry butt" or you can say "I expect only valid UTF-8 but I'm going to let you be a little sloppy and will throw an exception for you to catch and decide how you want to handle your invalid text." Both options are fine. Just be clear and consistent.

 

Personally, I'd prefer a library to use assert(). And provide a "isValidUtf8(const char* str)" or some kind of function so I can validate my text if I want. You can also use this function in your assert(). But that's just my personal preference.


[ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

#25 Hodgman   Moderators   -  Reputation: 29298

Like
3Likes
Like

Posted 21 February 2013 - 12:33 AM

Personally, I see the whole exceptions vs error-codes argument as too narrow, and results in complex code in either case. I'd prefer to unask the question, and instead just not have errors.
 
e.g. compare the three styles -- #1 error codes, #2 exceptions, #3 not having errors
uint Count();
//1
bool GetElementAtIndex( uint index, T& out ); // returns false and out is untouched if index is greater or equal to Count
//2
void GetElementAtIndex( uint index, T& out ); // index must be less than Count, or throws range_exception
//3
void GetElementAtIndex( uint index, T& out ); // index must be less than Count (error behaviour is undefined)
...
//1
bool TargetMonster( uint index, MonsterPool& monsters ) // returns true if index is valid
{
  m_target = NULL;
  return mosters.GetElementAtIndex( index, m_target );
}
//2
void TargetMonster( uint index, MonsterPool& monsters ) // if index is invalid, throws invalid_target
{
  try {
    mosters.GetElementAtIndex( index, m_target );
  }
  catch( range_exception& e ) {
    m_target = NULL;
    throw invalid_target(this);
  }
}
//3
void TargetMonster( uint index, MonsterPool& monsters )
{
  mosters.GetElementAtIndex( index, m_target );
}
With style #3, correct code works, and incorrect code has undefined behaviour (in development builds, that behaviour should be to break into the debugger or save a detailed crash dump).
This is the same as regular language features, like pointers -- e.g. pointers work in correct code, and in incorrect code you get undefined behaviour (e.g. dereferencing NULL pointers will likely crash).
 
If you're working with suspect data, e.g. if the user can type in an index for you to target, then correct code should sanitize the data before using it, e.g.
uint target = /*user input*/
if( target < monsters.Count() )
  player.TargetMonster( target, monsters );
else
  /* print error to user */

There aren't many instances where a constructor's failing is still recoverable, in my mind; a file stream failing to open a file is an example I can think of. One would need to check if the file was opened after the constructor returns; I find this acceptable. Opening a file has pretty much always been a two-stage affair: try to open it, then check if it is open.

In a game though, if a file is missing, there's not much you can do. If you get to level 3, but level3.model is missing, you can't fix that issue in a catch block, nor by checking a return code. You're dead if this error happens. Maybe you want to go back to the menu and show a dialog saying the game is broken, but in any case, the game is broken...
Maybe during development you want to have a bit of error tolerance to help people keep working on a half-finished game, in which case instead of returning an error at all, if you can't open the requested model, you can open error.model instead, and keep going (but if error.model is missing, you're definitely dead). In that case, I'd probably still use an assertion dialog so people are aware that the game is broken, but put a continue button on it (in the development build only -- I'd make the retail build just crash).

Edited by Hodgman, 21 February 2013 - 12:41 AM.


#26 Cornstalks   Crossbones+   -  Reputation: 6974

Like
0Likes
Like

Posted 21 February 2013 - 12:50 AM

@Hodgman: while I like option #3, what about situations outside of your control (for example, memory allocation failures)? Granted, these rarely happen in practice, but I feel like not handling them at all is... scary (security exploits?).


[ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]

#27 Hodgman   Moderators   -  Reputation: 29298

Like
2Likes
Like

Posted 21 February 2013 - 01:01 AM

what about situations outside of your control (for example, memory allocation failures)? Granted, these rarely happen in practice, but I feel like not handling them at all is... scary (security exploits?).

On consoles, memory allocation failures are completely in your control. You know how much memory you're limited to, so you have to design your game to fit in that limit. On PC they practically don't happen, unless you exhaust the address space somehow... You can prevent them causing your game from doing whacky things (like being security exploits) by terminating the app if an allocation fails.

Things like out-of-bounds array accesses can be security exploits, but they don't occur with correct code an sanitized data.
If you're not sure if any of the data in your system is valid, then inside the operator[] function (or any function that does something 'dangerous' with data), you need to perform a range-check/sanity-check every single time (and have some error handling code -- return codes, exceptions, etc)...

Alternatively, you can sanitize the data earlier, when it enters the system as a whole. If the data is trusted to begin with, and it's only transformed by correct code, then you can guarantee that it will continue to be trustworthy for all time without the need to constantly check for errors every time it's used... so, move your (permanent, not-stripped-in-retail-builds) error checking to places where data originates, not where it's used (and put your development-builds-only checking at the usage points for debugging purposes).

If you have genuinely recoverable construction errors, exceptions are pretty much your only option in C++

You can also use a "return code" via an out-argument, instructing the caller to promptly delete the newly constructed object without using it.

Edited by Hodgman, 21 February 2013 - 01:16 AM.


#28 wintertime   Members   -  Reputation: 1640

Like
0Likes
Like

Posted 21 February 2013 - 06:28 AM

I think one of the worst things in C was always lazy people ignoring error codes and crossing their fingers that nothing would go wrong. Thats because its a pain for the caller to always recheck errorcodes which are different for each function and it slows the program down even if nothing goes wrong, as there is always a check inside a function and outside and possibly many more levels of doubled effort for checking.

Exceptions are just thrown one time and if one likes to ignore them at least the program terminates instead of screwing up more and more, but the user of the function always gets the option of catching and doing a retry, trying a safer way to to it, filling in some standard data or terminating. Having to provide special callbacks for everything to gain this choice as the library user without exceptions is even more of a pain than checking errorcodes and if you want to recover you would then have to longjump around or set some global errorcode and return to the library which then returns to your outside code which then not only needs to check for an errorcode but for your threadunsafe global errorcode and remember to reset it.

Having to precheck everything you feed into a library function call is also painful doubled effort when the user is less likely to know all conditions under which the function accepts the data and also needs to replicate this check around every call, when the library then checks again to be safe from user error, if noone misguided disabled the assertions. If there is no check inside the library it gets inherently unsafe and your whole memory could get scrambled; dont tell me all bugs would always be found before disabling the checks or even without ever providing checks.

If you just feed everything into the library without prechecks you cant just let the library assert because it should be the main programs responsibility to decide how to proceed on errors and not the 'by many people with different usecases used and possibly aquired from somewhere else' library's. IMO there have been too many programs which just abort without a decent error message cause of such librarys and people using them wondering what happened. Quirky, painful to use or painful/impossible to configure error behavior is often the thing which makes me want to not use some library.



#29 swiftcoder   Senior Moderators   -  Reputation: 9845

Like
0Likes
Like

Posted 21 February 2013 - 07:08 AM


If you have genuinely recoverable construction errors, exceptions are pretty much your only option in C++

You can also use a "return code" via an out-argument, instructing the caller to promptly delete the newly constructed object without using it.


And, of course, static factory methods side-step this problem entirely.

Tristam MacDonald - Software Engineer @Amazon - [swiftcoding]


#30 samoth   Crossbones+   -  Reputation: 4677

Like
0Likes
Like

Posted 21 February 2013 - 07:53 AM

That said, I'm not a fan of exceptions. [...] They have been termed "invisible gotos", and I don't think that is an unfair characterisation.


Funnily, I'm a fan of exceptions, exactly for that reason. They are "invisible gotos", with the difference that they clean up. This allows you to write very nice and easy (and, efficient) code in some cases.
 

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


I've written initialization code similar to this before, worked like a charm:
try { audio = create_audio() } catch(int) { audio = &dummy_audio; }<br />
try { gl = create_gl4() } catch(int) { gl = create_gl3(); }

If something goes wrong (no sound card, OpenAL not installed, necessary extension not supported, whatever) the create function just throws. In the first case, the program uses a dummy implementation that doesn't produce sound (could be something else too). In the second case, if creating an OpenGL 4 context with all desired extensions fails, same is tried for an GL3 context, and if that one also throws, exception will make its way up to main where it's caught, an error is logged, and abort is called. Don't care what you have done, stack is unwound, RAII assures that someone cleans up your mess. If anything goes wrong, just throw, forget what you've done, and have a fresh start.

Of course you can do the same without exceptions, too. But it's not nearly as nice. Overhead? Even if there is one, this runs once during the lifetime of the program, so who cares. It's probably a matter of taste, too. But to me, personally, this is a great feature.

 

Imagine another case: A texture is missing. In a development build, this should probably be an assert. This is an obvious error, and it will fail again predictably the next time you launch the game, therefore it should fail hard so you're forced to fix it.

 

On the other hand, this really cannot ever happen when your game is shipped, so you need not check, right? Well, except if the user modifies your PAK files, or deletes something, or for whatever reason a file is lost. What now? Assert or crash? Check a return code every single time for something that really never happens?

 

Enter exceptions. An exception handler could replace the missing texture with a "fail texture" (e.g. red/white checkerboard) and output a message on the console. Is this perfect? Of course not, but the "piss off factor" of still being able to play more or less normally with a broken texture and knowing that something went wrong is much lower than getting "Your application has asked the runtime to terminate it".
 

performance


As for performance on general purpose PCs (I can't talk about consoles), exceptions are neglegible unless you do some very stupid things. If you don't throw ten thousand exceptions per second, it does not matter at all. And if you do, you haven't understood the "exceptional" part of "exception".

SJLJ exceptions admittedly have a fixed overhead for every try block, but the solution is simple: Spend a few seconds of thought before you add a million try blocks. It's the exact same thing as with virtual function calls. If you aren't outright playing stupid, they're not an issue. DW2 exceptions (GCC 4.x Win32 or Linux32/64) and SEH exceptions (MSVC / GCC 4.8 Win32/64) are practically "free".


Edited by samoth, 21 February 2013 - 07:55 AM.


#31 swiftcoder   Senior Moderators   -  Reputation: 9845

Like
0Likes
Like

Posted 21 February 2013 - 09:58 AM

In the second case, if creating an OpenGL 4 context with all desired extensions fails, same is tried for an GL3 context, and if that one also throws, exception will make its way up to main where it's caught, an error is logged, and abort is called.

There is no advantage here over just abort()'ing directly after failing to create the GL3 context. The OS is going to deal with freeing all your resources when you abort(), so why bother unwinding all the way to main?

 

In addition, you are handling the error right when it occurs - you might just as well inspect the return value.

Enter exceptions. An exception handler could replace the missing texture with a "fail texture" (e.g. red/white checkerboard) and output a message on the console.

And so could the code that reads a return value. There is nothing special about exceptions here - the magic is all in the handler. And woe betide the programmer who forgets to write said handler (or doesn't know he needs to)...

 

 

And if you do, you haven't understood the "exceptional" part of "exception".

This is perhaps my biggest pet-peeve about exceptions:

  • Did you write code that checks a condition and then throws an exception on failure? This isn't an exceptional situation, it's an anticipated and error-checked situation.
  • Did you write an exception handler? This isn't an exceptional situation either, it's an anticipated and handled error situation.

Edited by swiftcoder, 21 February 2013 - 09:58 AM.

Tristam MacDonald - Software Engineer @Amazon - [swiftcoding]


#32 samoth   Crossbones+   -  Reputation: 4677

Like
1Likes
Like

Posted 21 February 2013 - 11:04 AM

There is no advantage here over just abort()'ing directly after failing to create the GL3 context. The OS is going to deal with freeing all your resources when you abort(), so why bother unwinding all the way to main?


There's nothing wrong with doing a clean exit, and there is nothing wrong with code that is clear and concise. The mere fact that the OS will close your handles and such isn't really an excuse. If it was, you'd just do a *(void*)0; when the user selects "quit" from the menu. That will certainly quit the program, too  :-)

But more importantly, you're (deliberately, as I must assume) neglecting that printing out a one-line-message followed by abort is not what exceptions are meant for (even though this is what most people do anyway, almost all the time).

Aborting the process is the absolutely last thing you do, if there's no other options. If rudely killing the process when "something bad" happens is your intent, you can indeed just as well call abort. You'll have 8 keys less to type for the same effect.

 

Exceptions are for cases where you might have a chance of recovering in some way, or where you might at least have a chance of crashing less hard.

 

Such as trying another thing if the first (which is expected to work most of the time) fails. Or, for example, a program that has been writing to disk might want to exit cleanly even in the presence of a problem that prevents it from continuing -- instead of killing the process and forfeiting the data in the buffers. Or, it might want to delete all open (writeable) files under the assumption that they are possibly incomplete/unusable, and such files shouldn't stay around. The handler doesn't even need to know what went wrong or where, it doesn't care in this case.

 

This could of course also be done without exceptions, but not as cleanly, in one place, with a single handler for a hundred possible reasons.

woe betide the programmer who forgets to write said handler (or doesn't know he needs to)...


True, but... the same is true for return codes, except for one tiny detail. If someone doesn't check return codes, it "works" and doesn't work, and nobody can tell why. Or, it works for a while and then crashes at a completely unrelated occasion because some pointer is NULL. Or, it simply corrupts data and you never find out until 2 months after shipping when your boss starts yelling at you. Or, something different.

If someone forgets to handle exceptions, the application will be terminated with a human-readable message that is (hopefully) somewhat meaninful, at least to the guy who wrote the code. It will not go unnoticed.
 

This isn't an exceptional situation, it's an anticipated and error-checked situation. This isn't an exceptional situation either, it's an anticipated and handled error situation.


Both are true, and both are silly arguments against exceptions. Not the code that you write is exceptional, but the occurrance of an exception, that is, the condition failing.

 

Exceptions assume that your code and the data is "generally OK" and it "generally works", most of the time, and they are a way of still ensuring that something is done in the exceptional case when that's not true (and, without making the code unreadable, though what's "unreadable" and what is not is probably a very personal matter of taste, you might not agree on that part).

 

It would be really nice if  I magically didn't have to think about errors ever again, by flipping the "compile with exceptions" switch. But of course, you still have to write code that anticipates (and handles) failure.



#33 swiftcoder   Senior Moderators   -  Reputation: 9845

Like
0Likes
Like

Posted 21 February 2013 - 12:02 PM

True, but... the same is true for return codes, except for one tiny detail. If someone doesn't check return codes, it "works" and doesn't work, and nobody can tell why. Or, it works for a while and then crashes at a completely unrelated occasion because some pointer is NULL. Or, it simply corrupts data and you never find out until 2 months after shipping when your boss starts yelling at you. Or, something different.

Alexandrescu published Mandatory Error Codes way back in 2005, and C++11 move semantics remove the remaining ick factor from his implementation. I'm not sure why this managed to largely fly under the radar for the last few years, but it solves the problem very elegantly - just don't allow your clients to accidentally ignore error codes.

 

(incidentally, he included exception-safe constructors in the same article)


Tristam MacDonald - Software Engineer @Amazon - [swiftcoding]


#34 Bacterius   Crossbones+   -  Reputation: 8474

Like
0Likes
Like

Posted 21 February 2013 - 03:07 PM

But more importantly, you're (deliberately, as I must assume) neglecting that printing out a one-line-message followed by abort is not what exceptions are meant for (even though this is what most people do anyway, almost all the time).


The reason this is often done is because it is cleaner, and much harder to get wrong, than checking error codes after each goddamn function call and selectively figure out what needs to be done upon failure (e.g. send an error code through this and that socket, write something to the log file, delete such and such file, etc..). Bubbling the exception up the call stack provides an elegant and intuitive way of handling this, regardless of whether exceptions were meant for this or not. Many non-trivial applications cannot just dump core and require a complicated, data-dependent approach to failure (for recovery, cleanup, feedback, etc..) which would lead to a lot of duplicate code using error codes alone. 

 

If you want to find out what a modern language using only return codes looks like, check out Golang. The error handling is an absolute mess and makes code incredibly frustrating to read as each function call is accompanied by two or more boilerplate error-checking lines. Just because it works doesn't mean it's perfect, otherwise we would still be using 4-bit assembly "because it works". Though it is a matter of taste, to some extent.

 

As for error recovery, well, it depends on what your program is and how it is to be used. If it's a command-line tool, you probably don't want to waste time trying to guess the user's thoughts and attempt to recover every time. No, you just print out some error, crash, and let the user fix it. On the other hand, if you are writing a GUI utility program, if there's an error you want a dialog box to pop up saying that something went wrong, to let the user try again, rather than crashing to the desktop and letting the user guess where the log file is.

 

Of course, if you run out of memory, or some other general runtime failure, there is not much you can do. Doing anything nontrivial in your error handler will probably cause the program to fail again, sending it crashing and burning, which leads to all sorts of interesting effects, such as, say, deleting your entire home directory wink.png


The slowsort algorithm is a perfect illustration of the multiply and surrender paradigm, which is perhaps the single most important paradigm in the development of reluctant algorithms. The basic multiply and surrender strategy consists in replacing the problem at hand by two or more subproblems, each slightly simpler than the original, and continue multiplying subproblems and subsubproblems recursively in this fashion as long as possible. At some point the subproblems will all become so simple that their solution can no longer be postponed, and we will have to surrender. Experience shows that, in most cases, by the time this point is reached the total work will be substantially higher than what could have been wasted by a more direct approach.

 

- Pessimal Algorithms and Simplexity Analysis





Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS