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

C++ exceptions

Started by wcassella Jan 16, 2016 at 9:58 PM 52 replies 26k views
Original Post
wcassella
wcassella

Hi,

in various places around the internet I've seen strong recommendations against using exceptions in C++, for a variety of reasons (performance, semantics, code size, etc). Can anyone here make any comment on whether these arguments still hold today? I agree that exceptions aren't necessarily the best means of error handling, but in some cases they meet requirements that return-code style handling simply does not.

As game developers I'm sure many of you are biased towards disabling them completely (for the reasons above), but if that's your suggestion I'd appreciate it if you could supply hard evidence from updated compilers to support it.

Thanks

Brain
Brain
These days a lot of the concerns about using exceptions are no longer valid.

It used to be that enabling and using exceptions incurred a huge performance and memory hit.

These days the memory hit is tiny, and the performance implications not worth bothering about unless you're dealing in engine code within an AAA game where every cycle counts.

For the average indie game enable exceptions and use them where it is sensible. E.g. For exceptional circumstances as per their name.

Use an exception to indicate failure to initialise sound for example, but not failure to render an animation frame.

Be aware of exception safety for example when using constructors, destructors and new.

Have fun!
SeanMiddleditch
SeanMiddleditch

Yeah, I'll definitely fall into the camp you thought and disagree with braindigitalis here. :)

Exceptions are terribly problematic. This has far less to do with performance or overhead than you think. It has more to do with them being a feature that perverts basic control flow of an application in extremely difficult-to-predict ways. Writing exception-safe code is _immensely_ difficult, even with modern C++ features. There are very simplistic containers and algorithms that are literally impossible to make truly exception safe (as in offering the desired strong guarantee) as you can't ensure that all invariants will be maintained.

They're are a bad feature.. in _any_ language. They're bad in C# or Python or whatever, too. It's incredibly difficult to write correct code when you can't even trust the basic control flow of your code because it can be magically violated without clear annotation. It's not as bad as in C++ because of garbage collection (even with modern C++, move constructors/assignments can throw, because they might allocate, because the standards committee hates common sense or something), but they're still susceptible to container, algorithm, or object invariants being broken because of exceptions thrown at inconvenient times.

Making exception-safe code requires compromising your architecture (e.g. using the inefficient std::unordered_map instead of a sane closed hash table) and vastly complicating simple code.

The world has come a long way since exceptions were originally conceived; they're an unfortunate anachronism that are best brushed under the rug of history.

Sean Middleditch – Game Systems Engineer – Join my team!
frob
frob

Exception handling in C++ is more expensive than exception handling in some other languages where it is used frequently. There are costs to evaluating and unwinding the stack, cleaning up resources, and so on.

When it comes to standard conditions that are prone to happening it is generally less effort to handle it directly rather than through exceptions. Things happen. Files may not be there, rendering surfaces get lost, network connections time out. Prepare for it explicitly rather than using c++ exceptions.

Many places where C++ exceptions happen in games are either fatal conditions (like running out of memory) or conditions that could have been avoided through better practices (typecasting failure, improper use of a std::function or similar, out of range exception). This is in contrast to languages like Java where exceptions are more central to the language and have relatively less processing cost.

There are some penalties with using exceptions that you may not be aware of. Compilers generally won't inline functions with exception specifications. When you make a mistake with exception specifications the program will terminate(), instantly ending rather than gracefully handling the conditions. Templates generally have all kinds of problems with exception handling because the types are not easily known.

Making programs exception safe is very hard. Trying to keep your code at least exception neutral is difficult. Many of the most notable names in C++, like Bjarne Stroustrup and Herb Sutter, have written quite a few articles about it. Items need to remain invariant in the case of exceptions: if the exception happened in the middle of a sort, repairing the object's condition would mean unsorting to the original state; if the exception happened in the middle of assignment, repairing the object means it must be in the same state it was before the assignment operation started; if we allow for any object to throw in any operation a class containing multiple exception-throwing types cannot have strong exception-safe operations in even the most basic operations like assignment.

While exceptions can work out okay in languages like Java, and in some business code it might make sense to use C++ exceptions, extensive use of exceptions in C++ as used in games tends to introduce more problems than they are worth.

Brain
Brain
I do agree with what Sean said.

Exception safety is a complex beast, and these arguments remain.

It used to be that memory and performance were the main concerns of enabling exceptions as was the case with many c++ features we now use blindly without concern. In days of old inheritance was considered a memory and cpu intensive feature.

Do certainly be aware of the issues Sean raised, they are still valid and exceptions can be a minefield waiting to happen.

As I said use them sparingly and for exceptional circumstances. Don't fall into the java trap of having silly exceptions for any minor program condition! -- C++ is not java and is not C#! :)
Khatharr
Khatharr




The world has come a long way since exceptions were originally conceived; they're an unfortunate anachronism that are best brushed under the rug of history.

Except that STL and even core language features throw exceptions to communicate errors. There are many, many cases where exceptions are a massive PITA, but there are also cases where they work better than return values. Constructor errors are a problem, and requiring a return value to make room for an "error" value is not ideal. The new operator throwing by default instead of returning nullptr has always struck me as an incredibly dick move, but not nearly as dick as the syntax for non-throwing new..

If you plan to recover from any of this shit then writing exception safe code isn't really optional. That said, throwing and catching isn't really the problem here. Those are both pretty easy to do, and throwing is even downright convenient if you want to include an error message.

Fortunately, for game development recovery is usually not necessary. If you plan to write code for a mission critical server or something then you're stuck, but if you're just writing a game then most errors can CTD with an error message. That said, it would make sense to catch in main() and print e.what() or even just const char* in a dialogue, then throw descriptive error messages from wherever you want.

Of course, that's ignoring overhead, and in any case it might just be far more convenient to have a core language feature that could do the same job without the overhead or explicit catch.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.
frob
frob

The error cases in the c++ standard library that throw exceptions can be avoided easily:

Out of memory is a bad problem if you encounter that exception, but you shouldn't if you are managing memory correctly. If you start getting this your program will be crashing in the near future.

Out of range exceptions and length error exceptions mean you screwed up, and you shouldn't have gotten into that situation. There isn't much excuse for getting these because the game controls the ranges and lengths of items.

Invalid argument exceptions, similarly shouldn't be encountered in practice.

Bad cast exceptions can be avoided by using a different type of cast. No need to invoke the more expensive exception version when a different version exists that returns null.

Etc., etc.

SmkViper
SmkViper
C++ exceptions with modern compilers are zero cost (or very close to it) when not thrown. Exceptions can even improve performance by avoiding lots of checks and cache invalidations that checked error codes can cause.

They are more expensive when thrown.

Exceptions are the only way to return errors from constructors. Exceptions cannot be silently ignored. Exceptions make error handling obvious and easier to separate from function logic.

They are not to be used for normal control flow. (3dsMax, I'm looking at you... whose bright idea was it to use C++ exceptions to implement "return" in your scripting language?)

The "basic exception safety" guarantee is trivial to provide, assuming you are following good C++ coding practices and RAII, and put a tiny bit of thought into your interfaces. The "strong" and "no throw" guarantees can be much harder (and sometimes impossible) to enforce, but that's ok, because 99% of the time you don't ever need to provide those. People who complain that certain algorithms and containers cannot be made "exception safe" don't always realize they can't be made error safe either for the exact same reasons. After all, exceptions are just automated error return values separated from the normal return value.

People also tend to forget that not catching an exception is also perfectly acceptable. If you can't handle the exception, don't. If an exception happened, you only need to handle exceptions if you can somehow still fulfill your contract in the face of the particular exception - otherwise let the C++ language and RAII handle cleanup and get out of the way so someone else can handle it.

On the flipside there are the following concerns: Most game engines and code are not designed to be exception safe and do not properly use RAII and similar C++ mechanisms and so adding exceptions to them becomes a minefield. It is non-trivial to figure out what exceptions a function call may throw, without good documentation (Java tries to avoid this by specifying exceptions in the function header, but that tends to make things even worse). This is especially true in C++ when operator overloading is in effect. It is also sometimes hard to figure out when throwing exceptions is the correct thing to do.

In short, there are upsides and downsides to exceptions, just like any other feature in any other language. They are not a silver bullet for error handling, but neither are error codes or asserts as others would have you believe. C++ supports multiple forms of reporting errors, all of which should be considered and evaluated.

Above all - take everything you read (even this post) with a grain of salt. If you are happy and used to using error codes, keep using them (but you may want to at least consider following basic exception safety coding styles as they help prevent other kinds of bugs like memory leaks and memory corruption). If you want to try exceptions, go for it! And, like everything else, use your profiler if you care about performance.

Additional information about exceptions from Stroustrup, the creator of C++.
Hodgman
Hodgman
On old game consoles, the recommendation (or default behaviour) was to disable exceptions. C++ is supposed to have the philosophy of "only" pay for what you use, but exceptions created a cost across your entire program regardless of whether you ever threw or not. On CPUs with small instruction caches, this was a big deal, so we killed them off. That cost is still present, but not as noticeable on modern CPU's as it used to be.

As for throw itself, on x86 and a lot of other architectures, it is really slow to throw an exception, so they should never be used as a normal (expected) flow-control mechanism.
On x86-64, throw can be implemented a lot better, where now it's similar in cost to a virtual function call.

At least one of the current generation of games consoles now recommends that you never disable exceptions, because they use them liberally in their SDK...

If you are writing game engine / middleware code, I would recommend making it compatible with compilers that have disabled exceptions (i.e. don't use them, or hide them with macros that can be disabled) as many of your customers may still want to disable exceptions in their compiler.

As for the actual language side of things... Writing truely exception-safe C++ code is hard. Really hard. As in, no matter what you're doing, you have to be thinking about exceptions in depth for every single line of code that you write. Most people don't want to pay that mental tax...

There's 4 levels of guarantee you can give when writing a function. For the sake of example lets say that we're writing a function that's called when a grenade explodes -- it spawns a bunch of effects and damages nearby objects. Half way thru the function, an exception is raised.
1) no guarantee. Who knows how it behaves if an exception is thrown. This is all code until you've gone over it with a fine tooth comb... In our example, maybe thr grenade object is left with invalid invariants such that next time a function is called on it, the game crashes.
2) no leaks. Proper use of RAII makes this fairly simple. If something throws, you'll clean up all your resources. Program state still may have been modified though. e.g. we've damaged half the nearby objects, but not the other half. Ifthe caller tries again, those first half of the objects will receive double explosion damage. At least we wont crash though.
3) The strong guarantee -- aka transactional commit/rollback. The function either succeeds completely (returning without exception), or it makes no changes to the program state whatsoever. This is the really hard on in practice. E.g. If we're half way through our "do damage" loop when an exception is raised, we need to reverse back through that loop and re-add all the health points that we have remove so far. This gets harder as exceptions are thrown longer distances -- if a particle effect throws an out-of-memory, which is caught in the main game loop, then you need a mechanism to perform a roll-back of that entire frame!
4) the no-throw guarantee. No matter what, your function can never be involved in exceptional stack unwinding. This means that all the functions that you call must also provide this same guarantee (or they'll) infect your function. This one is hard to achieve in practice because there's no way to know if a 3rd party function may throw or not. Simply disabling exceptions at the compiler level magically bestows this guarantee on your entire code-base though :)


If exceptions are enabled, you should aim for level 3/4 guarantees. This will have a major architectural impact on your game. Most games are not built using a transactional model that supports easy roll-backs, making level 3 very painful to achieve.
This classification is also completely informal and not verified by the compiler - so you may think you're giving a level 3 guarantee, but are actually giving level 1 due to a subtle bug in your code.

C++ is much easier to read/write/maintain if we all just pretend that exceptions don't exist.
SmkViper
SmkViper

4) the no-throw guarantee. No matter what, your function can never be involved in exceptional stack unwinding.


I would instead put it as "this function will never throw an exception". A little clearer wording.

If exceptions are enabled, you should aim for level 3/4 guarantees.


Why? There is nothing wrong with the "basic guarantee" that says "nothing is leaked and I am in a valid, if unspecified, state".

As yourself and others have already stated, trying to provide the "strong" or "no-throw" guarantee is very hard, sometimes impossible, and usually has large penalties.

But there is little to no reason to go for it in a game. Heck, the standard library itself only provides "strong" or "no-throw" when it can do so for little to no cost, and guarantees to provide the "basic" level of exception safety across the board.
Hodgman
Hodgman

1) C++ exceptions with modern compilers are zero cost (or very close to it) when not thrown.
2) Exceptions can even improve performance by avoiding lots of checks and cache invalidations that checked error codes can cause.
3) Exceptions are the only way to return errors from constructors.
4) Exceptions cannot be silently ignored.
5) Exceptions make error handling obvious and easier to separate from function logic.

1) No, thid depends more on the target architecture than the compiler. A modern compiler for x86-64 can do cheap exceptions. For other architectures, not so much.
2) I'd like to see that benchmark. Exceptions are just syntactic sugar to automatically insert those checks for you (removing the human error of forgetting to propagate an error). They also insert these checks in places where they aren't needed (the compiler doesn't know if an external function call might throw or not), harming performance. In either case, the performance solution is to write APIs that don't rely on errors so much in the first place / can't encounter errors, not to micro-optimize your error handling.
3) No, there's lots of ways to return errors from a constructor. Exceptions are the only way to automatically roll back the memory allocation associated with the constructor. This is very error prone though, and nested constructor calls plus exceptions is a common source of leaks.
4) There's two ways to ignore exceptions which are both just as disastrous - catching with too wide of a base class, e.g. when somone adds catch(std::exception&){} to solve out-of-range bug, but now causes out-of-memory to get ignored too...
The other is simply failing to catch some kind of exception. Exceptions are invisible, such that any function can throw any class as an error, and there's no way at all to know this from the API. So a bit of middleware forgets to catch an internal::widget in some rare case, so they fail to tell you to catch it, so you don't, and now it bubbles up to main and kills your game. That's not a silent error, sure, but it's a silent API, which is just as bad.
5) yes the verbose syntax makes error handling code obvious, but as above, it completely hides all details of which errors occur when/where, moving that knowledge into documentation instesd of self-documenting code.

The right way to handle errors is one of those never ending debates older than Usenet itself...

[edit]

Why [go for the strong guarantee]? There is nothing wrong with the "basic guarantee" that says "nothing is leaked and I am in a valid, if unspecified, state".

As yourself and others have already stated, trying to provide the "strong" or "no-throw" guarantee is very hard, sometimes impossible, and usually has large penalties.

But there is little to no reason to go for it in a game.

The point of using exceptions over regular flow control is that some higher level algorithm further up the stack can resolve the error, but you can't. That high level code can then run the algorithm again (or abort) after the error is resolved.

If a resolvable error occurs half way through gameplay logic, is resolved, and then the algorithm (game rules) are applied again, it seems pretty important that the rules of the game stay consistent.
Doing double damage because an auto-save ran out of disk space should not be a feature of your error handling ideal.
King Mir
King Mir

The best arguement I've heard agains using exceptions is that they do hurt your worst case performance. If you have a rendering loop, exceptions thrown there will hurt your frame rate. In this case return value error cheching is better because the cost is comperable every frame.

I disagree that exception safety is hard. It is something to be aware of, but with good RAII single responsibility containers it's not a huge problem. It is however something that a programmer would need to learn if using exceptions in C++. I would be wary of people reccomending not using exceptions who don't know how to use exceptions: they don't know what they're missing.

In the non-error case, exceptions are faster than return type error handing. It also makes your code shorter. You handle error when you can do something about them; you never have to manually return an error code in a function when an a function inside it has as error condition; that's boilerplate you don't need to write with exceptions. Your code is arguably more clear as a result, because it plainly states the main path of execution.

In summary, I would reccomend enableing and using exceptions, except in cases were you care about worst case performance.

SeanMiddleditch
SeanMiddleditch

C++ exceptions with modern compilers are zero cost (or very close to it) when not thrown. Exceptions can even improve performance by avoiding lots of checks and cache invalidations that checked error codes can cause.


"zero cost" is a sadly untrue. Exceptions still require extra code or tables to be generated (binary size bloat) and suppress some degree of optimization (due to the more complicated scope destruction requirements). There's a reason that embedded developers still balk at use of exceptions, even with modern compilers.

That said, performance is not the real problem for most of us, so it's not a detail worth exploring in depth.

Exceptions are the only way to return errors from constructors.


Which should never actually be necessary. Also, constructors can take out-parameters; the constructed object must simply be left in a valid-for-destruction state upon error and you're fine, which is generally trivial to accomplish.

Exceptions cannot be silently ignored.


This is perhaps the one thing I'll give exceptions... and only because C++ lacks support for superior alternatives. You _can_ simulate less brain-damaged unignorable error semantics in C++ with a little runtime overhead, or just use the compiler-specific "don't ignore" attribute for return values, though, and get "close enough" for most uses.

There's also the use of monadic error states in objects. You see an example of this in C++'s standard I/O library. It's also the preferred mechanism for error states in most graphics or game libraries.

Exceptions make error handling obvious and easier to separate from function logic.


Very arguable. They only make it clear in the place that you handle the error. They have little improvement at the place errors are raised, and unlike _any_ other sane form of error handling, make it impossible to tell that an error is happening in intermediary functions in the call chain: exceptions are automatically propagated upward with no clear indication of where or when an exception might happen in a function's body. That's their biggest evil. Modern replacements require explicit per-statement or per-expression TRY markers to indicate that an expression which can fail is allowed to be invoked while still propagating the exception upward.

but that's ok, because 99% of the time you don't ever need to provide those. People who complain that certain algorithms and containers cannot be made "exception safe" don't always realize they can't be made error safe either for the exact same reasons.


No, we understand this perfectly. This is something I've done a non-trivial amount of research on lately specifically as I'm working on getting better containers pushed up into the C++ ISO standard, as the current ones (which all provide the strong guarantee) are very sub-optimal simply because they must be to provide the strong guarantee.

The basic exception guarantee is not good enough for most interesting container uses. If the result of the exception turns into "the whole data structure must be wiped clean before propagation" then the exception often equates to data loss. It is no longer safe to overwrite user files or continue a process with meaningful side effects in the face of such lost data. The only safe choice at that point is to abort the application.

So far as error-safe, yes, true, but most errors simply shouldn't propagate. If you're out of memory, just abort. Only a very very very tiny handful of applications have any reason to be tolerant of OOM errors, and those applications are typically written in C or an exception-free subset of C++ anyway.

The problem with exceptions is that they can be thrown without your knowledge. In C++, you can use noexcept to test for things, but you can't require noexcept in as many cases as you might like. C++'s std::string and most other containers are not guaranteed to have noexcept move semantics, for instance, because the standards gives implementors the freedom to use brain-damaged algorithms to implement the containers that always allocate on move or even default construction. (Which incidentally is one of the reasons to never use the STL and to reimplement most of it from scratch in a game; the standard intentionally has very weak quality requirements.)

On the flipside there are the following concerns: Most game engines and code are not designed to be exception safe and do not properly use RAII and similar C++ mechanisms and so adding exceptions to them becomes a minefield.


Most applications that _are_ designed to be exception safe fail horribly at it. Being exception safe is near impossible even for supposed experts. There have been numerous examples posted by leading industry experts of perfectly reasonable looking code that gets into a corrupted state (has broken invariants) in the face of common exceptions.

In short, there are upsides and downsides to exceptions, just like any other feature in any other language.


True.

That said, I maintain that the downsides vastly outweight the upsides and that the upsides only even exist because the committee keeps clinging to exceptions despite the ample evidence of superior alternatives (which sadly do require language additions to be executed well, hence their lack of popularity in C++). smile.png
Sean Middleditch – Game Systems Engineer – Join my team!
SeraphLance
SeraphLance




Items need to remain invariant in the case of exceptions: if the exception happened in the middle of a sort, repairing the object's condition would mean unsorting to the original state;

Does std::sort actually observe this? That's an enormous amount of overhead to be able to rollback an in-place sort (which I'm guessing most std::sort implementations are).

King Mir
King Mir

Items need to remain invariant in the case of exceptions: if the exception happened in the middle of a sort, repairing the object's condition would mean unsorting to the original state;

Does std::sort actually observe this? That's an enormous amount of overhead to be able to rollback an in-place sort (which I'm guessing most std::sort implementations are).

Std::sort just garuntees that the container is in a valid state when an expeption is thrown, but not that the partially sorted elements are in any particular order.

Servant of the Lord
Servant of the Lord

4) the no-throw guarantee. No matter what, your function can never be involved in exceptional stack unwinding. This means that all the functions that you call must also provide this same guarantee (or they'll) infect your function. This one is hard to achieve in practice because there's no way to know if a 3rd party function may throw or not. Simply disabling exceptions at the compiler level magically bestows this guarantee on your entire code-base though smile.png

Including dynamically linked DLLs or system calls that were compiled with exceptions enabled?

What I mean is, even if you have exceptions disabled in your code, third party function calls can still throw exceptions and crash your program, right?

Take operator new, for example. People sometimes mistakenly think that disabling exceptions means that 'new' returns null on failure. In reality, even if you disable exceptions, operator new still throws exceptions on failure. Only if you explicitly use new (std::nothrow) does it return null.

And if you call third party code like an OS function call or a call from a DLL you're linking to, if that function throws, it'll still get thrown, not be caught by your application, and just be caught by the OS that launched your application, and pop up a generic dialog box saying something like "YourGame has stopped working".

So basically, if you disable exceptions, unless you only call third party (and OS) functions that are also Level 4, isn't your 'entire code base' only providing Level 1 guarantee?

Hodgman
Hodgman

Including dynamically linked DLLs or system calls that were compiled with exceptions enabled?
What I mean is, even if you have exceptions disabled in your code, third party function calls can still throw exceptions and crash your program, right?

DLLs are a broken feature in C++, because C++ doesn't define a standard ABI, so there's no standard way to create a DLL that other people can use... which leads to the stupid situation where you need people to give you DLL's that have been compiled with your specific compiler, using your specific compiler settings.
The real solution there is to get the source code from your 3rd parties instead of just the compiled DLLs, in which case it becomes "your code" too ;)
OR
You use C DLLs, which means you shouldn't be throwing C++ exceptions across that boundary.

Same for system calls - they're usually a C API, which means it's nonsense to try and make a C++ exception cross that language boundary. I don't know of any OS API's that are C++.

Depending on your runtime under the hood, you may be making system calls into .NET/etc, which would throw it's own exceptions -- but these are not C++ exceptions, you would catch them with C++/CLI or C++/CX or whatever language your runtime is based on.

But yes, in the situation where you've disabled exceptions, it's more of an assertion that all of your code follows the no-throw guarantee. If that assertion fails, bad things will happen.

Take operator new, for example. People sometimes mistakenly think that disabling exceptions means that 'new' returns null on failure. In reality, even if you disable exceptions, operator new still throws exceptions on failure. Only if you explicitly use new (std::nothrow) does it return null.

In the kind of environment where you're passing the compiler flags to disable exceptions, you're also usually replacing the standard memory allocator with your own implementation smile.png
The new/delete keywords and malloc/free functions have been banned for almost every single project I've ever worked on professionally; instead being told to use a project/game/engine-specific macro, which can easily redirect all allocations for different builds.

King Mir
King Mir

If exceptions are enabled, you should aim for level 3/4 guarantees.

Obviously true if it's cheap to do so, but if an exception is thrown, something is wrong so rolling back the state may not be worthwile. This seems too broad a generalisation.

Also, the same rollback is needed regarless of your error handling mechanism. Exceptions don't change where you handle the error, they just take away the boilerplate of propigating it up.

C++ is much easier to read/write/maintain if we all just pretend that exceptions don't exist.

I disagree. Explicit value checking is harder to read because the main path of execution is more opaque, and because code is just plain longer.

You have to handle the error case whether you have exceptions or not, the only thing that exceptions make difficult is it's not explicit which code can fail. But the same code can fail with any aproach.

Hodgman
Hodgman

You have to handle the error case whether you have exceptions or not

I agree that exceptions aren't necessarily the best means of error handling, but in some cases they meet requirements that return-code style handling simply does not.

This also isn't an "either, or" situation. Exceptions or return-codes is a false dichotomy. There's so many more ways to think about program flow than those two options.
e.g. often in game middleware code I see a kind of pre-validate-and-assume-the-preconditions error handling mechanism.

Exceptions:


struct FooError : std::exception {};
class Foo { public: Foo(int size, int type;); };//throws FooError
void test(int size, int type) {
  try {
    Foo f( size, type );
  } catch( const FooError& f )
  {
    std::cout << "oh noes!";
  }
}

Error codes:


namespace FooError { enum Type { NoError=0, FooInvalid=1 }; }
 
class Foo { public: Foo(int size, int type, FooError& err); };
 
void test(int size, int type) {
  FooError::Type err;
  Foo f( size, type, err );
  if( err != FooError::NoError )
    std::cout << "oh noes!";
}

Validation and assertion of pre-conditions:


struct FooDescriptor { int size; int type; };
class Foo { public: Foo(const FooDescriptor& d) {ASSERT(Valid(d)); ... } static bool Valid(const FooDescriptor&); };
 
void test(int size, int type) {
  FooDescriptor d = { size, type };
  if( Foo::IsValid(d) )
    Foo f( d );
  else
    std::cout << "oh noes!";
}

IMHO, designing your code so that there's the least amount of error handling required is the best option, as then this debate gets less and less relevant -- the best way to handle errors doens't matter if you don't have any errors smile.png
If you rearrange the program flow so you know whether an operation is possible or not before attempting to do it, then your program will be a lot more manageable than one that gets half way through an algorithm before realizing you've asked the impossible, and then needs to carefully unwind it's way out of that mess.

In game middleware code, another common pattern that I see is two-pass algorithms that measure the resource requirements in the first pass, before actually running the algorithm in the 2nd pass. Normally, the algorithm might call 'new' 1000 times, each of which is a potential failure, but in this two pass mechanism, it never calls new at all!

Instead: the user calls pass#1 once, and then the user calls 'new' once to allocate a large block of memory. Then, if that allocation succeeds, the user passes that big chunk of memory to pass#2, which can now run the whole algorithm without any allocations.

Not only is that code more efficient due to calling 'new' 1000 times less (and having guaranteed contiguity of it's allocations for better locality of reference), but the algorithm itself now has no failure cases, and so there's no debate on how it should be dealing with out-of-memory errors. The problem simply evaporated by changing the program flow.

So instead of getting caught up on how to handle errors -- which IMHO is not seeing the forest for the trees -- design your code so that there are as few expected/unexpected error cases as possible in the first place. Most of the remaining errors should be fatal/unrecoverable errors, which need a completely different approach again!


but if an exception is thrown, something is wrong so rolling back the state may not be worthwile
If something is so wrong that you no longer care about having a stable/consistent game-state, then it's game over. At that point, it's basically an unrecoverable error, so you don't want to use error-codes or exceptions to handle it -- you want to use a crash handler!
King Mir
King Mir

4) the no-throw guarantee. No matter what, your function can never be involved in exceptional stack unwinding. This means that all the functions that you call must also provide this same guarantee (or they'll) infect your function. This one is hard to achieve in practice because there's no way to know if a 3rd party function may throw or not. Simply disabling exceptions at the compiler level magically bestows this guarantee on your entire code-base though smile.png

Including dynamically linked DLLs or system calls that were compiled with exceptions enabled?

What I mean is, even if you have exceptions disabled in your code, third party function calls can still throw exceptions and crash your program, right?

Take operator new, for example. People sometimes mistakenly think that disabling exceptions means that 'new' returns null on failure. In reality, even if you disable exceptions, operator new still throws exceptions on failure. Only if you explicitly use new (std::nothrow) does it return null.

And if you call third party code like an OS function call or a call from a DLL you're linking to, if that function throws, it'll still get thrown, not be caught by your application, and just be caught by the OS that launched your application, and pop up a generic dialog box saying something like "YourGame has stopped working".

So basically, if you disable exceptions, unless you only call third party (and OS) functions that are also Level 4, isn't your 'entire code base' only providing Level 1 guarantee?

Hopefully the libraries you consider will document if they use exceptions. But turning off exceptions does considerably restrict your library choices.

Topic Locked

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

Sign in to reply to this topic.