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