You are viewing pphaneuf

Previous Entry | Next Entry

Assertions and Exceptions

Smiling
wlach wrote an excellent article recently on how to use (and not use!) assertions properly, and it reminded me of some of my reflections on assertions and exceptions (warning: this is mostly written with C++ in mind, which does not have checked exceptions, no matter what you may think).

I would first like to emphatically support his first point: taking out an assert or turning it into a warning is not a "fix". A good developer will do a root cause analysis and find out why the pre-condition was being violated, since that is the real bug. I remember, a very long time ago, running GTK+ and GNOME programs from the command-line, seeing so-called "assertions" scroll past by the dozen, and thinking "oh my goodness, we are so doomed". I don't think Qt/KDE was much better either, but it's been a long time, and that's what I used to use. Now I start things from the menu, and I'm blissfully ignorant of how close to the cliff I'm dancing...

I used to despise exceptions, finding that they obscured the code path and made difficult the task of tracing through what is really happening in your code. It also forced me to litter my code with try/catch blocks, because as soon as you turn on exceptions (or rather, don't turn them off), anything can go wrong, at any time. What I learned later is two-fold.

First, like most tools (and C++ is very "good" at this, giving us very sharp, but dangerous tools!), exceptions can be abused, and most of the early code using exceptions that I met probably suffered a bit from the novelty aspect (it was a "sexy thing" back then, I guess), and over-used them massively. The second assertion mistake wlach talks about, using assertions for errors that may occur in the course of normal (or "non exceptional", if you will) program execution, applies to exceptions as well.

My current opinion is that functions that use exceptions to signal errors should also have a non-throwing alternative, for the cases where you do expect it. One example would be a string to integer conversion. Another can be taken from Boost, where there is two ways to convert a weak_ptr to a shared_ptr, an implicit way that throws and an explicit way that doesn't (but could give you a "null" shared_ptr and has to be checked for). The latter one is especially good design, since the "safer" exception throwing version is also the more implicit, "shoot from the hip" version, nicely counter-balancing each others.

Second, your code has to be exception safe. What this means is that getting an exception should not leave things in a bad state. Back in the days, we used raw pointers a whole lot, because this was how it was done, so basically any time that memory was allocated on the heap, you'd have had to wrap it in a try/catch block, so that if an exception happened, you'd free the memory on the way out. This was rather tedious, to say the least, and when you look back on it today, so was using raw pointers (and having to free memory manually). Nowadays, smart pointers rule the land, and it is incredibly easy to write exception-safe code with nary a try/catch block in sight, all appropriate cleanup being stowed out of sight in destructors.

So, exceptions, not so evil after all, but still should be used for exceptional conditions (big surprise!). But with the latter point, I was seeing a strong parallel with assertions, and in particular, those that I never want disabled. Now, think about it for a moment about what happens with unhandled exceptions: they call abort(), after writing a short message that tries to say what happened, just like an assertion. And defining NDEBUG doesn't touch the throws. That's exactly what I was looking for!

Not only that, but I now see many things that we did in WvStreams that have similar or better equivalents. For example, we added a crash dump and a "last will" feature. The former produced a text file (in addition to a potential core dump) when crashing, with a textual stack trace. The latter was a function you could call to set a string to be put in the crash dump in case something happened, to explain what was happening at that moment. GCC's default terminate handler manages to get at the exception object, so I guess it should be possible to do the same and put the information in the crash dump (this would be platform-dependent, but getting the stack trace already is, so this is not a big deal). The "last will" could also be implemented more efficiently by using a try/catch block and giving the "last will" information only in the case of an exception (making the non-exceptional path fast and quick, only having extra work in case it is really needed), then re-throwing (this is called "exception tagging", if I'm not mistaken). Note: as I mention to sfllaw in the comments, the "last will" cannot be replaced by a try/catch block, because some crashes are not through exceptions (segmentation faults, for example).

Also, in the event-driven multiplexing servers that WvStreams is usually used for, it's quite possible that an exception was only fatal to a single connection, and this gives the program the possibility of choosing a middle-ground between just logging a warning or dying altogether: it can now kill off the offending connection, log that event, and keep on going.

I still use assert(), but only for the more troubling things, such as detecting stack or heap corruption, where the only sane thing to do is really to abort the whole program. This is the kind of thing that is so exceptional that if someone disables it with NDEBUG, it wouldn't be the end of the world. I can put more expensive checks (such as canaries and magic cookies) that get disabled with NDEBUG, and at worst, leave a few "if (...) std::terminate();"

Finally, one thing that has long annoyed me were objects that have a method to know if the constructor had a problem, and where if it did, the object is invalid. Forgetting to check this state is a common source of bugs (especially in cases where the object is just instantiated on the stack), and this can often make the rest of the object's implementation more complex, having to check for validity on every method. Note that this validity check often should be an assert, IMHO.

Now, why carry this extra state all the time? This is an exceptional condition, adding an extra code path over the whole lifetime of your object, based on a single boolean value which will be set to the "valid" state 99.9% of the time, thus making sure that there's only a 0.1% chance alternative code path, you can picture what will be the test coverage of that! Exceptional condition, asserting on it, assertions should often be exceptions, hmm... Why not just tackle this at the source? Throw an exception in the constructor when the object would be invalid. The C++ runtime will free the memory, if it was heap-allocated. You'll have to be careful at object instantiation time, but you'd have to be anyway (checking the validity or wrapping in a try/catch block, pretty much the same overhead). If you ever forget, your program will still be correct, and will terminate, the exact same behaviour as if you didn't check the validity and called a method! Isn't that elegant or what?

The net footprint of that is a simpler implementation with fewer code paths to test, and zero real additional code for the user (the validity check is replaced with a try/catch)!

That's what convinced me that exceptions weren't so evil, when I found this case where they reversed the trend and gave me simpler code to understand than without exceptions, and that was more robust to boot. The try/catch block was perfectly unobtrusive, and in my simple test programs where I didn't care and would have let it assert(), it did too, but at the exact place where the object was deemed invalid (with more context to see why), instead of randomly later.

So, don't go forward and assert, but rather, go forward and throw!

Comments

( 13 comments — Leave a comment )
sfllaw
Oct. 15th, 2007 02:11 pm (UTC)
Well, catching non-exception based crashes like segfaults and such is still pretty valuable. Having that little stack trace makes life so much easier.

Just ask Ubuntu developers who use apport stack traces every day!
pphaneuf
Oct. 15th, 2007 02:16 pm (UTC)
Oh yes, definitely!

I was mostly reflecting on asserts, and things that can go away when NDEBUG is defined but that I wouldn't want to go away.

The "last will" is the one I wished could be fully replaced, though, it would be much nicer with a try/catch (dealing with it only when an exception actually happened), but that leaves segfaults and such out in the cold, so we can't do that.
sfllaw
Oct. 15th, 2007 02:48 pm (UTC)
Can "last will" actually be replaced? I suppose you'd need a new type of exception interface that can store a message as well as the stack trace before it gets unwound.

And then there's that mandatory try-catch around the entire programme. I suspect that a global must still be used to provide the automatic catch around main().
pphaneuf
Oct. 15th, 2007 03:40 pm (UTC)
Whether the "last will" can be replaced depends on how "throw;" (re-throwing the current exception) behaves. If the terminate handler that eventually gets called (remember that all along, I'm assuming unhandled exceptions, those that get handled wouldn't end up in WvCrash anyway) is still in the proper context to have a meaningful stack trace.

There's no mandatory try-catch around the program, as far as I know. If an exception bubbles up all the way to the main(), the "terminate" handler gets called and the program is aborted:

void foo()
{
        throw 42;
}

int main()
{
        foo();
}

(gdb) run
Starting program: /home/pphaneuf/tmp/foo 
terminate called after throwing an instance of 'int'

Program received signal SIGABRT, Aborted.
0xffffe410 in __kernel_vsyscall ()
(gdb) bt
#0  0xffffe410 in __kernel_vsyscall ()
#1  0xb7d7bdf0 in raise () from /lib/tls/i686/cmov/libc.so.6
#2  0xb7d7d641 in abort () from /lib/tls/i686/cmov/libc.so.6
#3  0xb7f7f270 in __gnu_cxx::__verbose_terminate_handler () from /usr/lib/libstdc++.so.6
#4  0xb7f7cca5 in ?? () from /usr/lib/libstdc++.so.6
#5  0xb7f7cce2 in std::terminate () from /usr/lib/libstdc++.so.6
#6  0xb7f7ce1a in __cxa_throw () from /usr/lib/libstdc++.so.6
#7  0x08048508 in foo () at foo.cc:4
#8  0x0804851e in main () at foo.cc:9


There's a bit of gunk on the way there, but you do have the exact line where the exception happened in the stack trace. I put something like this in my programs, it's pretty handy:

static std::terminate_handler old_handler;

static void backtrace_handler()
{
  void *stacktrace[512];
  backtrace_symbols_fd(
    stacktrace,
    backtrace(stacktrace, sizeof(stacktrace)/sizeof(*stacktrace)), 2);

  write(2, "\n", 1);

  old_handler();
}

void setup_backtrace()
{
  old_handler = std::set_terminate(backtrace_handler);
}


Then you call setup_backtrace() from your main(), just like with WvCrash.

The answer to my interrogation is that no, as soon as there is a handler, it unwinds the stack there, so my improvement on "last will" wouldn't work.
pphaneuf
Oct. 15th, 2007 02:23 pm (UTC)
Or...
One could throw an exception in the signal handler! Whee!!!
sfllaw
Oct. 15th, 2007 02:39 pm (UTC)
Re: Or...
Yes, but that's not so good either. After all, the nice thing about WvCrash and Apport is that they auto-catch the fault and give you a nice stack trace.
pphaneuf
Oct. 15th, 2007 02:49 pm (UTC)
Re: Or...
Yes, so does my "terminate" handler, because I like stack traces too!
sfllaw
Oct. 15th, 2007 02:57 pm (UTC)
Re: Or...
Well, if you segfault, there's no sane way to know whether you can still malloc() some decent memory. Whereas I'm pretty sure I implemented WvCrash such that you never had to construct an object and it was most obvious to use a static string.
pphaneuf
Oct. 15th, 2007 03:46 pm (UTC)
Re: Or...
Using exceptions does not require allocating memory. From what I understand, there's a kind of small stack area used for that. Of course, you still have to be careful, using something like WvString or std::string would be off-limits.
(Anonymous)
Oct. 15th, 2007 05:28 pm (UTC)
Delphi uses the "throw segfaults as exceptions" technique. It works surprisingly reliably, even if you try to keep going after the exception is thrown. Sure, continuing doesn't work every time, but it works a good 80% of the time (at least in a GUI application) and let me tell you, 5x less total application failures is a pretty positive experience for the user. (Naturally, we also report such exceptions back via a crash reporter, so they're *tracked* just as seriously as total failures.)

I'm actually curious how exception reporting in C++ avoids allocating memory, since that's obviously critical. In Delphi it's simple enough: there's one static area of memory for the current exception, and you can't have more than one outstanding exception at a time per thread, and that's that.

As far as re-throwing an exception to implement the "last will" feature, the question is whether the stack trace is captured when you "new" an exception or when you throw it. I'd expect the former, in which case it should be safe to just reassign the string pointer belonging to the exception, prepending the last_will on your way out. You'd have to use a static buffer, though.
pphaneuf
Oct. 15th, 2007 06:20 pm (UTC)
C++ on Windows also generates catchable exceptions for segfaults.

I'm not entirely sure of the details (such as whether it is possible to have more than one outstanding exception at a time per thread), but it's something out of an "emergency area", if I understand this correctly.

For the "last will", the stack trace is actually already unwound if you catch the exception in any way, so you've lost it. Basically, the only two things that get to see the proper stack trace are the exception's constructor (duh!), if any, and the "terminate" handler, and in the case of the latter, it gets the stack of the last throw or rethrow, so doing the "last will" thing makes it unusable.

Bummer.
(Anonymous)
Oct. 15th, 2007 11:06 pm (UTC)
...hmm, it sounds like it would work if you used a specially-defined Exception class, however. Not the most beautiful sounding technique, but it could be pulled off. Your special Exception could record the current stack trace inside itself at the time of construction; when you throw a new one, it grabs it from the stack, and when you need to rethrow, it can take one from another object if needed.

Pretty gross, though. Are you sure the stack trace is actually not part of the exception object in the first place? Kind of gross if not. I know Java, C#, and Delphi all have the stack trace inside the exception object itself.
wlach
Oct. 16th, 2007 07:57 pm (UTC)
I don't have anything to add, other than to say that it's a nice follow up to my original article. Thanks!
( 13 comments — Leave a comment )

Latest Month

March 2009
S M T W T F S
1234567
891011121314
15161718192021
22232425262728
293031    

Page Summary

Powered by LiveJournal.com
Designed by Lilia Ahner