niklas@appli.se (Niklas Hallqvist) (06/24/91)
I've come across a problem with the stack unwinding scheme used in the exception feature of C++. The key question is: Should a destructor be allowed to do any "real" work? By "real" I mean work which is not part of the constructor undoing. In my, maybe not so humble opinion, yes, a destructor should be allowed to be something like a scope exit function. Consider an object's autobiography: 1. I got born 2. I introduced myself to the world. 3. I worked. 4. I made my last will. 5. I died. Today, C++ supports the notion of a constructor which could do at least step 1 and maybe step 2 (The first thing an object does as a ready-made object). I used "maybe" because the object isn't really made, when you're IN the constructor body, but first afterwards the constructor has run. C++ also support a destructor-concept, which definitely supports step 5, and very often step 4 (the same restriction as for including step 2 in a constructor applies here). Now, when ANSI has sanctified exceptions, another problem occurs. Suppose you have an automatic object with a destructor doing some of those "last will"-decisions, you might want to control those decisions depending on the context. If it's in a stack unwinding because an exception has been thrown, you wan't to perform different things. The typical scenario is in a database application supporting transactions. Consider: class Transaction { // ... public: void begin_work(); void commit_work(); void rollback_work(); }; int bar(); // returns 1 if OK, 0 otherwise void foo() { Transaction t; t.begin_work(); if(bar()) t.commit_work(); else t.rollback_work(); } Now, if the bar() provider rewrites bar() to throw an exception if bar() is not successful, and returns ordinarily otherwise: void bar(); // Might throw WRITE_FAILED, NO_MEMORY How do we rewrite our code? First of all we note what's been said in the ARM 15.3c and add another class: class MyTransaction { Transaction& t; public: MyTransaction() { t.begin_work(); } ~MyTransaction() { if(/* in exception context */) t.rollback_work() else t.commit_work(); } }; // Hmm, wait a minute, how do I do that? Well, I'll let it be for now... void foo() { MyTransaction t; bar(); } As you can see, we've got a problem here, and the cause is of course that we've moved the real work into the destructor to be safe from the potential exceptions bar() might throw. The usual solution is to write a try block everywhere transactions are used, and it will of course work, but it'll be a lot of redundant code since if the exceptional condition could be checked-for in the destructor, only one check will be made, instead of many scattered out through the application. This possiblity would serve as a nice encapsulation of exceptional handling. How could we achieve this? My ideas are: 1. Make a distinction between the "last will" of an object and the actual destruction, by providing another special member function. This scheme could also be used to separate actual construction, and the 2:nd step mentioned above. The "2:nd step"-function should be executed after all the constructors of an object has been run, and "this" should therefore have the dynamic type "pointer to most derived class", hence allow virtual function calls to work as expected. The "last will" function should be overloadable on possibly thrown exception objects. 2. Overload destructors on the thrown objects type, i.e. make destructors take the thrown exceptions as arguments. 3. Introduce a class catch-clause which got entered before the destructor. Well... What do you net.people say? Will this be such an unusual situation so you won't worry, or do you feel like me, now when exception handling has come to stay, the destructor of an object should be able to find out the context it got called from? Another interesting side-effect of allowing this is that it should be plausible to throw exceptions from a destructor without having to worry about if an exception has already been thrown. Niklas Niklas Hallqvist Phone: +46-(0)31-40 75 00 Applitron Datasystem Fax: +46-(0)31-83 39 50 Molndalsvagen 95 Email: niklas@appli.se S-412 63 GOTEBORG, Sweden mcsun!sunic!chalmers!appli!niklas -- Niklas Hallqvist Phone: +46-(0)31-40 75 00 Applitron Datasystem Fax: +46-(0)31-83 39 50 Molndalsvagen 95 Email: niklas@appli.se S-412 63 GOTEBORG, Sweden mcsun!sunic!chalmers!appli!niklas
jat@xavax.com (John Tamplin) (07/01/91)
In article <1561@appli.se> niklas@appli.se (Niklas Hallqvist) writes: ... discussion of problems with destructor during exception handling ... >class Transaction { > // ... >public: > void begin_work(); > void commit_work(); > void rollback_work(); >}; > >int bar(); // returns 1 if OK, 0 otherwise > >void foo() { > Transaction t; > t.begin_work(); > if(bar()) > t.commit_work(); > else > t.rollback_work(); >} > >Now, if the bar() provider rewrites bar() to throw an exception if >bar() is not successful, and returns ordinarily otherwise: > >void bar(); // Might throw WRITE_FAILED, NO_MEMORY > >How do we rewrite our code? First of all we note what's been said >in the ARM 15.3c and add another class: > >class MyTransaction { > Transaction& t; >public: > MyTransaction() { t.begin_work(); } > ~MyTransaction() > { if(/* in exception context */) t.rollback_work() else t.commit_work(); } >}; > >// Hmm, wait a minute, how do I do that? Well, I'll let it be for now... > >void foo() >{ > MyTransaction t; > bar(); >} > >As you can see, we've got a problem here, and the cause is of course that >we've moved the real work into the destructor to be safe from the potential >exceptions bar() might throw. The usual solution is to write a try block >everywhere transactions are used, and it will of course work, but it'll >be a lot of redundant code since if the exceptional condition could be >checked-for in the destructor, only one check will be made, instead of >many scattered out through the application. This possiblity would serve >as a nice encapsulation of exceptional handling. How could we achieve this? >My ideas are: I have done almost the exact thing without trouble. My example: class MyTransaction { Transaction &trans; public: MyTransaction(Transaction &t) trans(t) { trans.begin_work(); } ~MyTransaction() { if(trans.in_progress()) trans.rollback_work(); } void commit() { trans.commit_work(); } void abort() { trans.rollback_work(); } }; void foo() { Transaction t; ... non-transaction stuff ... { MyTransaction mt(t); bar(); mt.commit(); } } If your implementation of Transaction can't tell you if it is in progress, you can add a state flag to MyTransaction and eliminate the need. I think that this methodology also solves the general case where you want the destructor to do different things based upon previous actions -- you just tell the object before it gets destructed. >Well... What do you net.people say? Will this be such an unusual situation >so you won't worry, or do you feel like me, now when exception handling has >come to stay, the destructor of an object should be able to find >out the context it got called from? Another interesting side-effect of >allowing this is that it should be plausible to throw exceptions from a >destructor without having to worry about if an exception has already been >thrown. I think the C++ compiler should already be able to handle that, at least according to my interpretation. -- John Tamplin Xavax jat@xavax.COM 2104 West Ferry Way ...!uunet!xavax!jat Huntsville, AL 35801