Exceptions
Posted: Thu Jul 09, 2015 8:32 pm
The following is mainly intended as a question to Falco but if anyone else has opinions or suggestions regarding the topic raised I am, of course, most interested in those, too.
I decided to ask this when i tuned in a little late to Falcos livestream on Monday, just in time to hear him express a certain disdain for exceptions with a statement akin to "nobody has time for that overhead" and going on to mention they are turned off when compiling Elysian Shadows
(I might have missed the question being addressed already since i, as mentioned, tuned in quite late and couldnt stay very long due to the timezone difference and other obligations. If this is the case I apologize for the inconvenience).
I am not going to dispute this choice, which is in my view a reasonable and rational one. Exceptions, while useful in some cases, not only complicate a programs control flow significantly, but even violate one of the most important aspects of C++'s design philosophy: you should not pay for what you dont use. Whilst no other language feature(except RTTI) has, at least to my knowledge, any cost attached in instances where it is not directly utilized, exceptions do add overhead for every single function that might potentially have an exception thrown(directly or indirectly through another function called in its body). Even though the most popular ways of implementing them, often dubbed "zero-cost" exceptions, guarantee no additional code is excecuted when nothing is thrown, their name is nonetheless a lie, because, at least in remotely modern architectures, additional code does not necessarily have to be executed to have a runtime cost attached to it
(Due to interesting issues like icache misses and some optimizations being unavailable in the face of exceptions. Here are some interesting details about exception implementations for interested readers ).
Aside from most games, even llvm/clang, one of the best written open source c++ projects known to me, forbids exception use
Those are more than enough reasons to justify turning off exception handling, especially when writing time critical code for resource constrained systems.
(Even so I would still propably measure this cost on an individual basis to check if it really is significant(which it may very well not be in many cases))
I am, however, quite interested in how you are dealing with the complexities switching off exceptions globally entails, which is not quite as obvious. Despite their apparent lack of popularity, they are nonetheless an integral part of the language. A fact not only reflected in the standard library, but even in one far too frequently (ab)used language feature that normally reports errors via throwing: new.
In general, C++ has a very nice and well designed library(with a few exceptions, like the return value of binary_search) and I really like using it. However, since exceptions are a part of the language and disabling them is, as a matter of fact, a non-standard extension(offered by almost every compiler) many parts of said library rely on them for error reporting and the behaviour without them is not quite clear. gcc has a detailed explanation of how it handles support in standard library headers, which basically boils down to:
Sadly I could not find appropriate documentation for libc++ or other implementations quite as easily, so even so one might reasonably assume they do something similiar, one can never be quite sure without looking at the actual implementation, which emphasises the portability problem caused
(a casual look into some libc++-headers seems to reveal that all exception related code is wrapped with #ifndef _LIBCPP_NO_EXCEPTIONS. I didnt really bother to look deep enough to know what happens to normal new statements
(it would be interesting to know if they are automatically replaced with the nothrow variantion (for gcc/libstdc++ this seems to be dependent on which library version is linked ) )).
But all is not lost. For almost every case a standard library function throws, the preconditions could be checked manually(for example std::vector::at vs std::vector::operator[]) and/or nothrow alternatives exist(for example std::stol vs directly calling std::strtol).
Those without alternatives seem to fall into two basic categories, one of which sadly quite common in containers:
Fortunately, constructors throwing for reasons other then memory allocation failures are kind of rare, even more so in the standard library and do not prove too much a problem(Off the top of my head I can think of only one, std::regex)
The second problem, memory allocation, is a more serious one. Despite the fact that there is a nothrow version of the dreaded new, which simply returns a nullptr on failure, no standard container could use it. To provide the ultimate flexibility we all know and love C++ for, containers have one additional template type, the allocator type, which allows control over dynamic allocations. To acquire memory, the container calls the allocators allocate(), to free it uses the respective deallocate() member, being blissfully unaware of all the gory details that actually happen, which are safely hidden away and fully configurable. This is a wonderful thing(I find Howard Hinnant's stack based allocator especially impressive), even more so since C++11 significantly simplified the required interface, but that very interface proves to be problematic without exceptions. According to the standard, the pointer returned by allocate must point to a valid region of memory that can be used to hold the specified number of objects of the desired type. nullptr does, of course, not satisfy this requirement and may therefore never be returned. Since this is guaranteed, containers may - and will - rely on this return value being valid and simply construct objects without further inspection. Without exceptions, there is no way to prevent them from doing so(whilst keeping the program alive).
To further complicate matters, most containers pre-allocate more memory than actually needed (in order to reduce the number of allocations needed and fullfill some amortized complexity guarantees), which is why even with knowledge of the amount of available memory it is not generally possible to check preconditions in advance.(http://stackoverflow.com/questions/4826 ... ation?lq=1)
The only real "solutions" I could perceive would be the following:
So, with apologies for the long prelude, I will finally state my actual question:
How do you deal with this problem in ES? Is there some way around it that eludes me and if so, would you care to share it? Am I overthinking the issue? Does anyone know how other projects banning exceptions deal with those issues?
(As always, I beg your forgiveness for the wall of text and I am most sorry if this turns out to be a stupid question)
I decided to ask this when i tuned in a little late to Falcos livestream on Monday, just in time to hear him express a certain disdain for exceptions with a statement akin to "nobody has time for that overhead" and going on to mention they are turned off when compiling Elysian Shadows
(I might have missed the question being addressed already since i, as mentioned, tuned in quite late and couldnt stay very long due to the timezone difference and other obligations. If this is the case I apologize for the inconvenience).
I am not going to dispute this choice, which is in my view a reasonable and rational one. Exceptions, while useful in some cases, not only complicate a programs control flow significantly, but even violate one of the most important aspects of C++'s design philosophy: you should not pay for what you dont use. Whilst no other language feature(except RTTI) has, at least to my knowledge, any cost attached in instances where it is not directly utilized, exceptions do add overhead for every single function that might potentially have an exception thrown(directly or indirectly through another function called in its body). Even though the most popular ways of implementing them, often dubbed "zero-cost" exceptions, guarantee no additional code is excecuted when nothing is thrown, their name is nonetheless a lie, because, at least in remotely modern architectures, additional code does not necessarily have to be executed to have a runtime cost attached to it
(Due to interesting issues like icache misses and some optimizations being unavailable in the face of exceptions. Here are some interesting details about exception implementations for interested readers ).
Aside from most games, even llvm/clang, one of the best written open source c++ projects known to me, forbids exception use
Those are more than enough reasons to justify turning off exception handling, especially when writing time critical code for resource constrained systems.
(Even so I would still propably measure this cost on an individual basis to check if it really is significant(which it may very well not be in many cases))
I am, however, quite interested in how you are dealing with the complexities switching off exceptions globally entails, which is not quite as obvious. Despite their apparent lack of popularity, they are nonetheless an integral part of the language. A fact not only reflected in the standard library, but even in one far too frequently (ab)used language feature that normally reports errors via throwing: new.
In general, C++ has a very nice and well designed library(with a few exceptions, like the return value of binary_search) and I really like using it. However, since exceptions are a part of the language and disabling them is, as a matter of fact, a non-standard extension(offered by almost every compiler) many parts of said library rely on them for error reporting and the behaviour without them is not quite clear. gcc has a detailed explanation of how it handles support in standard library headers, which basically boils down to:
- - replace try with if(true)
- replace catch with if(false)
- replace throw with abort
(- ignore exception specifications)
Sadly I could not find appropriate documentation for libc++ or other implementations quite as easily, so even so one might reasonably assume they do something similiar, one can never be quite sure without looking at the actual implementation, which emphasises the portability problem caused
(a casual look into some libc++-headers seems to reveal that all exception related code is wrapped with #ifndef _LIBCPP_NO_EXCEPTIONS. I didnt really bother to look deep enough to know what happens to normal new statements
(it would be interesting to know if they are automatically replaced with the nothrow variantion (for gcc/libstdc++ this seems to be dependent on which library version is linked ) )).
But all is not lost. For almost every case a standard library function throws, the preconditions could be checked manually(for example std::vector::at vs std::vector::operator[]) and/or nothrow alternatives exist(for example std::stol vs directly calling std::strtol).
Those without alternatives seem to fall into two basic categories, one of which sadly quite common in containers:
- 1. Constructors
2. Functions which might allocate memory(stuff like insert/push_back/resize/reserve/etc.)
Fortunately, constructors throwing for reasons other then memory allocation failures are kind of rare, even more so in the standard library and do not prove too much a problem(Off the top of my head I can think of only one, std::regex)
The second problem, memory allocation, is a more serious one. Despite the fact that there is a nothrow version of the dreaded new, which simply returns a nullptr on failure, no standard container could use it. To provide the ultimate flexibility we all know and love C++ for, containers have one additional template type, the allocator type, which allows control over dynamic allocations. To acquire memory, the container calls the allocators allocate(), to free it uses the respective deallocate() member, being blissfully unaware of all the gory details that actually happen, which are safely hidden away and fully configurable. This is a wonderful thing(I find Howard Hinnant's stack based allocator especially impressive), even more so since C++11 significantly simplified the required interface, but that very interface proves to be problematic without exceptions. According to the standard, the pointer returned by allocate must point to a valid region of memory that can be used to hold the specified number of objects of the desired type. nullptr does, of course, not satisfy this requirement and may therefore never be returned. Since this is guaranteed, containers may - and will - rely on this return value being valid and simply construct objects without further inspection. Without exceptions, there is no way to prevent them from doing so(whilst keeping the program alive).
To further complicate matters, most containers pre-allocate more memory than actually needed (in order to reduce the number of allocations needed and fullfill some amortized complexity guarantees), which is why even with knowledge of the amount of available memory it is not generally possible to check preconditions in advance.(http://stackoverflow.com/questions/4826 ... ation?lq=1)
The only real "solutions" I could perceive would be the following:
- - Either do not use the standard containers at all
- or use custom allocators everywhere(which you propably do anyway) and accept the fact that the only reasonable action on allocation failure is to optionally log/throw some violent insult at the perpetrator and then abort everything
So, with apologies for the long prelude, I will finally state my actual question:
How do you deal with this problem in ES? Is there some way around it that eludes me and if so, would you care to share it? Am I overthinking the issue? Does anyone know how other projects banning exceptions deal with those issues?
(As always, I beg your forgiveness for the wall of text and I am most sorry if this turns out to be a stupid question)