[comp.lang.c++] Destructors & Exceptions

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