Sign in to follow this  
PhilMorton

new and Exceptions

Recommended Posts

Say im writing an industrial-strength uptime-critical application (such as a large important server), which has to be as fault-tolerant as possible - Should every instance of 'new' be surrounded by a try-catch block incase I run out of memory? (this app may be running on limited resources) If this is the case, wouldnt it be easier to check for this error condition of bad_alloc's if new just returned NULL, instead of throwing an exception? I've heard about this no_throw typedef that you can pass to new to make it do this, but I've never used it so far. Is the sytax something like "Foo* f = new(no_throw) Foo();"? Also, if this app is also performance-critical, is there going to be a performance advantage from using a "no throwing new" instead of putting exception handlers everywhere?

Share this post


Link to post
Share on other sites
Wrapping everything in try/catch blocks doesn't solve the problem. The proper solution is to write robust exception-safe code. Unfortunately for you, this is much more difficult than just try/catching everywhere.

A very, very brief summary might be: throw when you need to, catch where you can, and where you have to.

RAII, copy-swap and related idioms can minimize the amount of "work" you need to do in the face of an exception, and make your code more robust and fault-tolerant to boot.

A number of GotW articles on the subject.
The C++ FAQ also covers the area.

Share this post


Link to post
Share on other sites
Quote:
Original post by jpetrie
Wrapping everything in try/catch blocks doesn't solve the problem. The proper solution is to write robust exception-safe code. Unfortunately for you, this is much more difficult than just try/catching everywhere.

A very, very brief summary might be: throw when you need to, catch where you can, and where you have to. Use RAII and related idioms to minimize the amount of "work" you need to do in the face of an exception.

A number of GotW articles on the subject.
The C++ FAQ also covers the area.


Heh, i just finished reading that C++ FAQ before I posted this ;) [edit]thanks for the GotW link tho![/edit]

I'm not wrapping everything in try/catch's... I'm just wondering about memory allocation errors, and the best way to handle them.

At the moment I havnt run into any problems where I need to throw my own custom exceptions (im sure the time is approaching tho), but I know new can throw if it cant allocate the requested memory, and some functions where I am using new need to have the no-throw guarentee.

Share this post


Link to post
Share on other sites
If a function that allocates memory has to guarantee it can't throw, then you have to suppress the exception. It is still better to suppress it via a catch (instad of nothrow), because you'll likely want to catch any other operations that could throw and try to handle them, if you can (allowing an exception to propagate beyond a function with the throw() specifier will call unexpected() and shoot your app dead).

Oh, and...

Quote:

Also, if this app is also performance-critical, is there going to be a performance advantage from using a "no throwing new" instead of putting exception handlers everywhere?


No, not really. Exception handling code is generated whether or not you use them -- an exception could still pass through your code and appropriate action needs to be taken.

Share this post


Link to post
Share on other sites
Quote:
Original post by PhilMorton
Say im writing an industrial-strength uptime-critical application (such as a large important server), which has to be as fault-tolerant as possible -

Should every instance of 'new' be surrounded by a try-catch block incase I run out of memory? (this app may be running on limited resources)


If by "surrounded" you mean right there (e.g. try { ptr = new T; } catch (std::bad_alloc) { ... }) then no. If you plan on handling allocation failures individually right at that point, you should use the "nothrow" version of new as you mention:

T * ptr = new (std::nothrow) T;
if ( !ptr ) {
/* allocation failure */
}


Far saner, however, is to use RAII construct (smart pointers, etc) so you can deal with the problem at a higher point in the logic:

void serve_request() {
socket s;
try {
s.open( ... );
boost::scoped_ptr< A > a( new A );
boost::scoped_ptr< B > b( new B );
std::vector< C > c( 1000 );
...
} catch ( std::bad_alloc ) { //called if ANY of those allocations failed
s << "HTTP/1.1 505\n\n" << ...; //semi-bullshit HTTP headers off the top of my head
s << "Server running low on memory - retry request later";
s.close();
} catch ( const socket_lib_error & e ) {
error_log << "Socket failed: " << e.what() << std::endl;
} catch ( const std::exception & e ) {
error_log << "Unknown exception caught: " << e.what() << std::endl;
page_admin();
}
}




Quote:
Also, if this app is also performance-critical, is there going to be a performance advantage from using a "no throwing new" instead of putting exception handlers everywhere?


"Depends". Some compilers have a (relatively small) cost for entering and leaving try blocks, so you definately don't want to pepper these throughout the inner loops of your raytracer - such a thing is asking for trouble and usually isn't a good idea anyways. This is part of the reason I used the nothrow version when checking the single allocation and dealing with a failure there immediately. The actual act of throwing the exception (and unwinding the stack) is definately on the "expensive" list too, so you're definately not going to want to use it for (relatively) commonly occuring operations.

That said, returning and checking error codes also has a small performance cost, so using exceptions can actually be a little faster when well placed (at higher levels of logic execution like above, for rarely occuring conditions).

Share this post


Link to post
Share on other sites
Quote:
Original post by MaulingMonkey
If by "surrounded" you mean right there (e.g. try { ptr = new T; } catch (std::bad_alloc) { ... }) then no. If you plan on handling allocation failures individually right at that point, you should use the "nothrow" version of new as you mention:
T * ptr = new (std::nothrow) T;
if ( !ptr ) {
/* allocation failure */
}
Thanks, yeah thats what I meant. Most of the code that i'm using this error checking in is fairly low-level and handles errors right as they happen.
Quote:
Original post by MaulingMonkey
Far saner, however, is to use RAII construct (smart pointers, etc) so you can deal with the problem at a higher point in the logic
Scott Meyers books recently convinced me of how important RAII is, so now im learning about auto_ptr and the boost pointer templates etc :D

Quote:
Original post by jpetrie
If a function that allocates memory has to guarantee it can't throw, then you have to suppress the exception. It is still better to suppress it via a catch (instad of nothrow), because you'll likely want to catch any other operations that could throw and try to handle them, if you can (allowing an exception to propagate beyond a function with the throw() specifier will call unexpected() and shoot your app dead).
Thanks. So I guess i should only use the no_throw technique if Im certain that no other important exceptions are going to be supressed by it.

Share this post


Link to post
Share on other sites
Quote:
Original post by PhilMorton
So I guess i should only use the no_throw technique if Im certain that no other important exceptions are going to be supressed by it.


They won't.

Cool factoids based on VS2005 Standard on my local machine:

VS2k5 can throw and catch ~126k exceptions/second on my machine (based on a 1 million exception sampling) in default release mode settings.

VS2k5 was able to warn me (when compiling for release mode - probably due to link time code generation) that the catch statements in my other two exception benchmarking tests that the code would never be reached, even though the actual implementation of the function being called was in a seperate translation unit with no throwing specifiers.

VS2k5 didn't warn me about the equivilant error-based version, oddly.

In release mode, 1 million iterations of the benchmark took "0 seconds" for everything but said throwing function.

In debug mode, the results were:

Benchmark                 |     Errors |   Expected |       Time |
Local try/catch + throw | 1000000 | 1000000 | 11.453 |
Local try/catch | 0 | 0 | 0.078 |
Nonlocal try/catch | 0 | 0 | 0.063 |
Error codes | 0 | 0 | 0.078 |
Error codes + errors | 1000000 | 1000000 | 0.062 |


Note: The timing mechanism used was boost::timer, I'm uncertain how coarsely grained it is.

Switching back to release mode + 1,000,000,000 iterations (disabling the first test) I got:

Benchmark                 |     Errors |   Expected |       Time |
Local try/catch + throw | 0 | 1000000000 | 0 |
Local try/catch | 0 | 0 | 0.593 | # ~1,686,340,640 entries/leaves from try blocks/second
Nonlocal try/catch | 0 | 0 | 0 |
Error codes | 0 | 0 | 0 |
Error codes + errors | 1000000000 | 1000000000 | 0 |


At this point I needed to upgrade to a 64 bit loop in preperation to increase my iterations by another factor of x1000 for tests 3-5. However, even without the x1000 increase, my performance results were altered drastically enough I realized there was no signal to be found in the noise:

Benchmark                 |     Errors |   Expected |       Time |
Local try/catch + throw | 0 | 1000000000 | 0 |
Local try/catch | 0 | 0 | 1.468 |
Nonlocal try/catch | 0 | 0 | 1.5 |
Error codes | 0 | 0 | 1.422 |
Error codes + errors | 1000000000 | 1000000000 | 1.547 |


Benchmarking code used (obviously contrived, and thus flawed to some degree or another - alterations described above involve #if 0 ... #endif-ing out test bodies to disable them, and later, using unsigned __int64 instead of just unsigned in 32 bit mode):

#include <boost/timer.hpp>
#include <boost/lexical_cast.hpp>
#include <iostream>
#include <iomanip>

void do_throw(); //other.cpp implementation: { throw std::exception(); }
void do_no_throw(); //other.cpp implementation: {}
unsigned return_0(); //other.cpp implementation: { return 0; } //"success"
unsigned return_1(); //other.cpp implementation: { return 1; } //"error"

template < typename BenchT , typename ErrorsT , typename ExpectedT , typename TimeT >
void benchmark_display( const BenchT & benchmark
, const ErrorsT & errors
, const ExpectedT & expected
, const TimeT & time
)
{
std::cout << benchmark << " | "
<< std::setw( 10 ) << errors << " | "
<< std::setw( 10 ) << expected << " | "
<< std::setw( 10 ) << time << " | "
<< std::endl;
}

int main () {
const unsigned loops = 1000000;
boost::timer t;

//--- Benchmark 1 - local try/catch block, exceptions throw
unsigned local_try_catch_block_throwing_caught = 0;
t.restart();
for ( unsigned i = 0 ; i < loops ; ++i ) {
try { do_throw(); }
catch( const std::exception & ) { ++local_try_catch_block_throwing_caught; }
}
double local_try_catch_block_throwing_elapsed = t.elapsed();

//--- Benchmark 2 - local try/catch block, exceptions not thrown
unsigned local_try_catch_block_caught = 0;
t.restart();
for ( unsigned i = 0 ; i < loops ; ++i ) {
try { do_no_throw(); }
catch( const std::exception & ) { ++local_try_catch_block_caught; } //warning C4702: unreachable code
}
double local_try_catch_block_elapsed = t.elapsed();

//--- Benchmark 3 - nonlocal try/catch bloc, exceptions not thrown
unsigned nonlocal_try_catch_block_caught = 0;
t.restart();
try {
for ( unsigned i = 0 ; i < loops ; ++i ) {
do_no_throw();
}
} catch( const std::exception * ) { ++nonlocal_try_catch_block_caught; } //warning C4702: unreachable code
double nonlocal_try_catch_block_elapsed = t.elapsed();

//--- Benchmark 4 - error codes, returning 0 ("success")
unsigned errors_using_return_0 = 0;
t.restart();
for ( unsigned i = 0 ; i < loops ; ++i ) {
if ( return_0() ) { ++errors_using_return_0; }
}
double errors_using_return_0_elapsed = t.elapsed();

//--- Benchmark 5 - error codes, returning 1 ("failure")
unsigned errors_using_return_1 = 0;
t.restart();
for ( unsigned i = 0 ; i < loops ; ++i ) {
if ( return_1() ) { ++errors_using_return_1; }
}
double errors_using_return_1_elapsed = t.elapsed();

benchmark_display( "Benchmark " , "Errors" , "Expected" , "Time" );
benchmark_display( "Local try/catch + throw " , local_try_catch_block_throwing_caught , loops , local_try_catch_block_throwing_elapsed );
benchmark_display( "Local try/catch " , local_try_catch_block_caught , 0 , local_try_catch_block_elapsed );
benchmark_display( "Nonlocal try/catch " , nonlocal_try_catch_block_caught , 0 , nonlocal_try_catch_block_elapsed );
benchmark_display( "Error codes " , errors_using_return_0 , 0 , errors_using_return_0_elapsed );
benchmark_display( "Error codes + errors " , errors_using_return_1 , loops , errors_using_return_1_elapsed );
}



(The machine in question was running XP Home w/ a 2.4 GHz P4)

Share this post


Link to post
Share on other sites
If you're using an older version of Visual C++, another thing to be aware of is that new might return NULL instead of throwing.

Quote:
Beginning in Visual C++ .NET 2002, the CRT's new function (in libc.lib, libcd.lib, libcmt.lib, libcmtd.lib, msvcrt.lib, and msvcrtd.lib) will continue to return NULL if memory allocation fails. However, the new function in the Standard C++ Library (in libcp.lib, libcpd.lib, libcpmt.lib, libcpmtd.lib, msvcprt.lib, and msvcprtd.lib) will support the behavior specified in the C++ standard, which is to throw a std::bad_alloc exception if the memory allocation fails.

Normally, if you #include one of the C++ standard headers, like <new>, you'll get a /defaultlib directive in your object that will reference the appropriate C++ Standard Library according to the CRT model you used (the /M* compiler options). Generally, that will cause the linker to use the throwing operator new from the C++ Standard Library instead of the nonthrowing one from the main CRT, because the order of defaultlib directives will cause libcp.lib to be searched before libc.lib (under /ML).

Even if you use one of the C++ standard library headers, it is possible to get non-throwing new.


This apparently has been changed in 2005.

Quote:

Checking the result of new is a bug in C++

At least, it is a bug in VC8. That check won't happen. Reading Larry Osterman's recent posts "What's wrong with this code, part 15" and the answers, reminded me this behavior changed in VC8. If you check the result of new in code compiled with VC8, your code is wrong. The call to new will throw. Yes, your code worked in 5.0, 6.0, (maybe) 7.0 and 7.1, but it doesn't now.

You can get the old behavior if you link with nothrownew.obj. The linker will then make every call to new in your dll (or exe) non-throwing. This is incompatible with STL, or any other code that relies on new throwing.


Of course, if you're not using Visual C++ this doesn't apply to you. And i'm not sure if this also applies to 2003.

Share this post


Link to post
Share on other sites
Quote:
Thanks, yeah thats what I meant. Most of the code that i'm using this error checking in is fairly low-level and handles errors right as they happen.


Handled *how*, exactly? What are you planning to *do* if you run out of memory? If it's "if anything at all can't be allocated, then report an error and roll back the global state to where it was before the request", then (a) you may as well do it at a higher level (i.e. try { handleRequestFromUser(args); } - because otherwise you'll be producing huge amounts of code duplication and manually forwarding error conditions back to callers in order to communicate the problem all the way back to the output module - BTW this is exactly the sort of thing the exception mechanism is supposed to do for you automatically if you just leave it alone!); and (b) you will *need* to write exception-safe code (although this will probably be pretty important *anyway*).

Quote:

Thanks. So I guess i should only use the no_throw technique if Im certain that no other important exceptions are going to be supressed by it.


No; you want to use it where no exceptions *at all* are going to be *generated* locally. The no_throw technique (a) doesn't really suppress exceptions (it simply chooses not to emit them when it otherwise would) and (b) can't "suppress" anything besides std::bad_alloc. And any exception *at all* out of a function declared as throw() will blow up.

Share this post


Link to post
Share on other sites
Quote:
Original post by PhilMorton
Say im writing an industrial-strength uptime-critical application (such as a large important server), which has to be as fault-tolerant as possible -


Then maybe you shouldn't use C++...

OK.

Have you thought about a using custom new_handler (which is called on allocation failure)? The default one just raises bad_alloc (yes, that's where the exception ultimately comes from), but you may have something smarter you can do.

Share this post


Link to post
Share on other sites
Furthermore, have you considered what you actually can do if new fails? Remember that, whatever it is, it can't allocate any memory at all. Usually, all you can do is bail. GOTW has a series on this as well.

Share this post


Link to post
Share on other sites
Quote:
Original post by PhilMorton
Also, if this app is also performance-critical, is there going to be a performance advantage from using a "no throwing new" instead of putting exception handlers everywhere?


Here's the implementation of new(nothrow) from GCC's libstdc++ (new[](nothrow) forwards to this function as well).

void *
operator new (std::size_t sz, const std::nothrow_t&) throw()
{
void *p;

/* malloc (0) is unpredictable; avoid it. */
if (sz == 0)
sz = 1;
p = (void *) malloc (sz);
while (p == 0)
{
new_handler handler = __new_handler;
if (! handler)
return 0;
try
{
handler ();
}
catch (bad_alloc &)
{
return 0;
}

p = (void *) malloc (sz);
}

return p;
}



The default value of __new_handler is NULL, so the try block will not be entered if you have not taken steps to handle out-of-memory situations gracefully by supplying your own new_handler.

Your best bet if you're trying to write a daemon under the constrainst you've described is to follow these rules.

(1) Write your code using established exception-safe techniques and assume that you will never run out of memory.

(2) Handle std::bad_alloc at an appropriate high level that can log a message and shut down your application gracefully.

(3) Supply a new_handler (std::set_new_handler() is found in <new>) that takes appropriate action under low memory situations. If you can't write such a beast, then there's probably nothing you could do if you either caught std::bad_alloc or checked for NULL every time you used new.

I would suggest that you do not attempt to write industrial-strength time-critical applications in C++ without having read (and pretty much memorized) all three of Scott Meyers's "Effective C++" books and both of Herb Sutter's "Exceptional C++" books (which are extended and detailed versions of the GOTW columns).

I do write industrial-strength time-critical server applications in C++ for a living. Pretty much every time I encounter a bug it's because one of the guidelines in one of those books was violated (it's surprising how a manager's 'just make it work and have it done by last week' can make you forget what you've learned).

Share this post


Link to post
Share on other sites
Quote:
Original post by Zahlman
Handled *how*, exactly? What are you planning to *do* if you run out of memory? If it's "if anything at all can't be allocated, then report an error and roll back the global state to where it was before the request"...

What if memory allocation failed not because "nothing at all can be allocated", but because the limited heap size is too fragmented to allocate a contiguous block of the size that I asked for - This is bound to happen at some point on a PS2 if the client of your game engine pushes the limits to far, for example. In this case, it may still be possible to allocate several smaller blocks...

Share this post


Link to post
Share on other sites
Quote:
Original post by Bregma
(3) Supply a new_handler (std::set_new_handler() is found in <new>) that takes appropriate action under low memory situations. If you can't write such a beast, then there's probably nothing you could do if you either caught std::bad_alloc or checked for NULL every time you used new.

I would suggest that you do not attempt to write industrial-strength time-critical applications in C++ without having read (and pretty much memorized) all three of Scott Meyers's "Effective C++" books and both of Herb Sutter's "Exceptional C++" books (which are extended and detailed versions of the GOTW columns).


Thanks for the tip..

BTW i mentioned earlier in the topic that I have been reading Scott Meyers books. "Effective C++" is a feaking gold mine - many interviews I've taken at games companies have been based allmost entierly on that book!

Share this post


Link to post
Share on other sites
Quote:
Original post by PhilMorton
Quote:
Original post by Zahlman
Handled *how*, exactly? What are you planning to *do* if you run out of memory? If it's "if anything at all can't be allocated, then report an error and roll back the global state to where it was before the request"...

What if memory allocation failed not because "nothing at all can be allocated", but because the limited heap size is too fragmented to allocate a contiguous block of the size that I asked for - This is bound to happen at some point on a PS2 if the client of your game engine pushes the limits to far, for example. In this case, it may still be possible to allocate several smaller blocks...


If you're hitting that situation and it's actually possible to *use* several smaller blocks, you might want to rethink your container usage and/or look in to pool allocators? :\

Share this post


Link to post
Share on other sites
Quote:
Original post by Zahlman
If you're hitting that situation and it's actually possible to *use* several smaller blocks, you might want to rethink your container usage and/or look in to pool allocators? :\

Thanks, will do.

Anyway, we're pretty far off topic here. I was just asking about the best way to detect bad_alloc's... Not whether i really need to do so in the first place, or whether it is possible for me to continue if a bad alloc occurs, or whether the way im allocating memory is the most appropriate for my situation (which is mostly unknown as far as the question goes), or whether my code is exception safe, or whether ive taken the level at which the error should be handled into account...

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this