The lie of C++ exceptions
As part of the ongoing work on NGEDIT, I’m now establishing the error management policy. The same way that I’m refactoring the existing code to use my new encoding-independent string management classes, I’m also refactoring it to a more formal error handling policy. Of course, I’m designing along the way.
Probably my most solid program (or, the one on which I felt more confident) was part of a software system I developed for a distribution company about 9 years ago. The system allowed salesmen to connect back to the company headquarters via modem (the internet wasn’t everywhere back then!) and pass on customers’ orders every evening. I developed both the DOS program that ran on their laptops, and the server that ran on AIX. I developed the whole system in C++ – gcc on AIX, I can’t remember what compiler on the DOS side. Lots of portable classes to manage things on both sides. As a goodie, I threw in a little e-mail system to communicate between them and with hq, which was out of spec – and I managed to stay on schedule! It was a once-and-only-once experience, as mostly all my other projects have suffered of delays – but the project I had just done before was so badly underscheduled and underbudgeted that I spent weeks nailing the specs to not fall in the same trap.
The part I felt was most important to keep solid was the server part – salesmen could always redial or retry, as it was an interactive process. The server part was composed of a daemon that served incoming calls on a serial port, and a batch process that was configured to run periodically and export the received files to some internal database system.
How did I do the error management? I thought through every single line in the process, and provided meaningful behavior. Not based on exceptions, mind you. Typical processing would involve sending out a warning to a log file, cleaning up whatever was left (which required its own thinking through), and returning to a well-known state (which was the part that required the most thinking through). I did this for e-v-e-r-y s-i-n-g-l-e high-level statement in the code. This meant: opening a file, reading, writing, closing a file (everyone typically checks file opens, but in such a case I felt a failure in closing a file was important to handle), memory management, all access to the modem, etc…
C++ brought exceptions. I’m not 100% sure yet, but I think exceptions are another lie of C++ (I believe it has many lies which I haven’t found documented anywhere). It promises being able to handle errors with much less effort, and it also promises to allow you to build rock-solid programs.
The deal is that exceptions are just a mechanism, and this mechanism allows you to implement a sensible error handling policy. You need a rock solid policy if you really want to get failproof behavior, and I haven’t seen many examples of such policies. What’s worse, I haven’t yet been able to figure out exactly how it should look like.
Furthermore, exceptions have a runtime cost, but the toughest point is that they force you to write your code in a certain way. All your code has to be written such that if the stack is unwound, stuff gets back automatically to a well-known-state. This means that you need to use the RAII technique: Resource-Acquisition-Is-Initialization. This covers the fact that you have to relinquish the resources you have acquired, such that it doesn’t leak them. But that is only part of returning to a well-known state! If you are doing manipulation of a complex data structure, it’s quite probable that you will need to allocate several chunks of memory, and any one of them may fail. It can be argued that you can allocate all memory in advance and only act if all that memory is actually available – but then, this would force your design around this: either you concentrate resource acquisition in a single place for each complex operation, or you design every single action in your design in two phases – first one to perform all necessary resource acquisition, second one to actually perform the operation.
This reminds me of something… yeah, it is similar what transaction-based databases do. Only elevated to the Nth degree, as a database has a quite regular structure, and your code usually doesn’t. There are collections, collections within collections, external resources accessed through different APIs, caches to other data-structures, etc…
So, I think in order to implement a nice exception-based policy, you have to design a two-phase access to everything – either that, or an undo operation is available. And you better wrap that up as a tentative resource acquisition – which requires a new class with its own name, scope, declaration, etc…
Not to talk about interaction between threads, which elevates this to a whole new level…
For an exceptions-based error-handling policy, I don’t think it is a good design to have and use a simple “void Add()” method to add something a collection. Why? Because if this operation is part of some other larger operation, something else may fail and the addition has to be undone. This means either calling a “Remove()” method, which will turn into explicit error management, or using a “TTentativeAdder” class wrapping it around, so that it can be disguised as a RAII operation. This means any collection should have a “TTentativeAdder” (or, more in line with std C++’s naming conventions, “tentative_adder”).
I don’t see STL containers having something like that. They seem to be exception-aware because they throw when something fails, but that’s the easy part. I would really like to see a failproof system built on top of C++ exceptions.
Code to add something to a container among other things often looks like this:
void function(void) { //... do potentially failing stuff with RAII techniques ... m_vector_whatever.push_back(item); // ... do other potentially failing stuff with more RAII techniques }
At first, I thought it should actually look like this:
void function(void) { //... do potentially failing stuff with RAII techniques ... std::vector<item>::tentative_adder add_op(m_vector_whatever, item); // ... do other potentially failing stuff with more RAII techniques add_op.commit(); }
But after thinking a bit about this, this wouldn’t work either. The function calling this one may throw after returning, so all the committing should be delayed to a controllably final stage. So we would need a system-wide “commit” policy and a way to interact with it…
The other option I see is to split everything in very well defined chunks that affect only controlled areas of the program’s data, such that each one can be tentatively done safely… which I think requires thinking everything through in as much detail as without exceptions.
The only accesses which can be done normally are those guaranteed to only touch local objects, as those will be destroyed if any exception is thrown (or, if we catch the exception, we can explicitly handle the situation).
And all this is apart from how difficult it is to spot exception-correct code. Anyway, if everything has to be done transaction-like, it should be easier to spot it – suddenly all code would only consist in a sequence of tentatively-performing object constructions, and a policy to commit everything at the “end”, whatever the “end” is in a given program.
I may be missing something, and there is some really good way to write failproof systems based on exceptions – but, to date, I haven’t seen a single example.
I’ll keep trying to think up a good system-wide error handling policy based on exceptions, but for now I’ll keep my explicit management – at least, I can write code without enabling everything to transaction-like processing, and be able to explicitly return stuff to a well-known safe state.
This was my first attempt at a shorter blog entry – and I think I can safely say I failed miserably!
November 17th, 2005 at 5:06 pm
How will you improve your example if you didn’t use exceptions?
One more thing you are forgetting is that there are three exception guarantees:
1. Basic – no resource leaks, object remains in a valid state.
2. Strong – same as basic, but object remains in the state before and after the expception is thrown.
3. No throw – no exceptions can be thrown.
In your post you are only talking about strong guarantee, and forgetting the other two. You must decide what guarantee is appropriate for each of your classes, when building a system that uses exceptions.
November 17th, 2005 at 7:00 pm
curious,
Thanks for the pointer – I’ve started to research that framework based on guarantees. I’m probably less educated in C++ than I should, but I do dedicate a lot of thinking to try to find the fundamental issues.
It actually seems the transaction semantics are described, with non-throwing in the “commit” phase being a need. At early research, I haven’t seen a description of how a full framework should operate and its application in each situation – the guarantees are also tools to implement the error handling system, and I’m trying to figure out the whole policy.
Anyway, I’m starting to see a way that exception-based error handling could work. Do you have a pointer to some kind of complete description?
I think that, as a rule, a function needs to guarantee state to a any data which is not local (globals – yes they exist -, function arguments, and any other data obtained through them). So it must do a kind of “transaction based” operation on them. Member variables modified from methods have to be treated like that as well.
Regarding your question about my “improvement”, it goes along the simple line of:
if (m_vector_whatever.push_back() == RET_OK)
{
// Explicit handling of everything: undoing, restoring well-known state, …
}
I would probably be willing to move to exceptions if I can figure out the whole error generation framework – I think that even if exception handling currently impacts runtime performance, it could be optimized and it will probably be in future compilers (using whole-program analysis).
November 18th, 2005 at 12:35 am
Heya, excellent thoughts, really. Regarding a pointer into the right direction, have you read “Exceptional C++” by Herb Sutter? Also, Abrahams over at boost is an exception handling guru and has written down a thing or two.
November 18th, 2005 at 5:40 pm
Sebastian, I haven’t read that book. I’ve been researching the subject since yesterday, and one of the articles I came up with after searching for “c++ exception strong guarantee” was by Abrahams. I also read some stuff by Herb Sutter.
I plan on following up with the issue. I actually might try to nail it down, and reach a conclusion, at least with regards to my needs. Ie, conclude what it would mean to write my C++ code based around exceptions, and make my mind up for once.
I have the intuition, although I’m not sure yet, that it is actually more involved than explicit error handling – I mean, in an objective way.
November 24th, 2005 at 7:57 pm
[…] The growing pains of NGEDIT A blog on the development of the NGEDIT text editor « The lie of C++ exceptions […]