I should have known! If you touch a dogma, you're accused of being a heretic. Or even worse you are assumed ignorant. The issue of exceptions and destructors seems to be a dogma of the C++ programming community. However, I'll try again. Like every heretic, I'm a true believer of my ideas. So, I'll start from the very beginning: First, there was C++. Then exceptions were introduced...
I still don't know if it was such a good idea. Of course, you need a safe longjmp(), and exceptions are exactly this, but to introduce them as the Standard way of error handling might have raised more problems than it solved. But, now we have exceptions, and we don't have a choice. Even if you don't use exceptions in your own classes, some other classes in your application might throw exceptions, the standard library (or any other used library) throws exceptions, and any other application that reuses your class might throw exceptions as well. Therefore, you really have to live with exceptions (unless you have absolute control of your current and any future instance of your program and you use a compiler switch to disable exceptions).
First, consider what exception specifications are. Exception specifications definitely state what exceptions a function might throw. In C++, each function has an exception specification. If it doesn't have an explicit one, it has an implicit one. This implicit exception specification says that a function might throw any exception. Sadly, you cannot say explicitly the same in the C++ syntax. Probably it would have been a good idea to allow something like "throw(...)", but what the standards committee has omitted from the syntax you can add as a comment, like "int f() // throw(...)".
As the user of a function, you must use Murphy's Law and assume, that the function will throw an exception unless it explicitly guarantees by an empty exception specification that it never will do. So, in general, you have to make your program exception safe. (What this means in detail, and to what extent a function or a class must be exception safe, is a full story on its own).
As the designer of a function, you have to specify which exceptions your function might throw. Unfortunately, if you know your function might throw a value of MyException, you cannot simply state "throw(MyException)", as this would guarantee that nothing else will be thrown, and as "throw(MyException, ...)" is not allowed, you're stuck in most cases with the comment version (and no explicit exception specification).
As the designer of the interface of a (member) function, you must be aware that you set up a contract that the implementation of your function and all possible polymorphic implementations in derived classes must fulfill now and in all future.
Now, after this long introductory sermon, I will return to your articles.
What are the responsibilities of software design (and software architecture, to use a current buzzword)? First, the design must ensure that the required functionality (which to find and describe is the task of analysis, but this is another story again) is provided by the software system.
But then, it is another duty of design to produce a software architecture that is flexible against future changes and extensible with future improvements. Again another goal of software design might be the easiness of reusing some parts of the system in another application. So, you have to design interfaces that stay stable while the (polymorphic) implementations must be allowed to change.
The goal of function and class design is to provide an interface that provides as much functionality as possible to users of the class, while at the same time imposing as few constraints as possible on the implementations. This is where exceptions come in.
An empty exception specification definitely provides better usability of a function, but it imposes constraints on the implementations, which might be very hard to keep for some implementations, especially if your interface is a polymorphic one. Worse still, it probably conflicts with another important quality of a software architecture: uniformity. If an architecture decides to handle a specific type of event by a specific C++ exception, this should hold true for the whole system. But, if some implementation of your interface raises such an event, it is forced, by an empty exception specification, to cope with the event in a non-conforming way.
The introduction of a throwing and a non-throwing variant, as you proposed in your article, might be a nice feature, but it could be a source for more problems than it solves. If there exist both variants right from the start, any (library) function might choose to call the non-throwing variant, and the (exception using) application which must call the given function will loose the exception though it is well prepared to cope with it.
If there is no throwing variant defined in the beginning, this problem is even worse, and it might not be possible to add the other variant later (e.g. the source is not available). So, as a result, my own conclusion is that empty exception specifications should be used with extreme care only. If they are used in the interface of base classes, they nearly always cause serious design problems in the future. And frankly said, if I see such an empty exception specification with a virtual function, I just don't believe it, but make my own code, which uses that function exception safe anyway. And even with non-base classes, implementations might change. So, I nearly always avoid empty exception specifications.
What about exception specifications with a specific exception class?
In general, this is not desirable as well. Such an exception specification doesn't buy you anything on the usability side, as any function calling your function must be exception safe anyway, and it imposes mainly the same constraints on implementations as empty exception specifications. So, if an implementation knows that it might throw a specific exception, it definitely should document this in its interface, but not by using a formal exception specification, but by using the comment style. But as a conclusion, a non-empty formal exception specification is even more questionable than an empty one.
All arguments of the above discussion still hold true for destructors. And never forget that an empty exception specification for a destructor not only imposes constraints on the destructor but on the whole class implementation. So, formal exception specifications should be avoided for destructors as well, especially in base classes where the destructor must be virtual. Consequently no user of a class may safely assume that the destructor does not throw.
But the fact that you should avoid exception specifications for destructors as well does not mean that you should throw from destructors lightly. I think, that you should generally avoid throwing exceptions from destructors. But uniformity as well as other design issues might force you to..
You must also be aware of all the additional issues this might rise, some of which are: extra resource control, control of additional exceptions if your destructor was called during stack unwinding, missing destructor calls if a destructor throws that was called during deleting of arrays, etc. So, this is the bottom line: take extreme care if you use exception specifications, take extreme care if you want to throw from destructors, and make your functions exception safe even with respect to exceptions thrown from destructors.
Overload Journal #28 - Oct 1998 + Programming Topics
|Browse in :||
All > Topics > Programming (768)
Any of these categories - All of these categories