Portable Use And Disabling Of Exceptions

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

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.

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

Advertisement

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?

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:

[source]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
[/source]

Another option:
[source]
// 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
}
[/source]

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?
[size=2][ 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 ]

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.

[size=2][ 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 ]
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).

@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?).

[size=2][ 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 ]

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.

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.


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. Ex-BigTech Software Engineer. Future farmer. [https://trist.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".

This topic is closed to new replies.

Advertisement