www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - scope + destructor with Exception parameter for RAII

reply Leandro Lucarella <llucarella integratech.com.ar> writes:
What do you think of adding an optional parameter (exception) to the 
destructor, defaulting to null, to indicate the destructor was called 
then unwinding because an exception was thrown? Then you can almost 
forget about scope(exit/success/failure) and you have a RAII as complete 
  as Python's 'with' statement.

So you can do something like:

class Transaction
{
	Database db;
	public:
	this(Database d) { db = d; db.begin(); }
	~this() // success
	{
		db.commit();
	}
	~this(Exception e)
	{
		db.rollback();
	}
	// ...
}

scope Transaction t = new Transaction(db);

This concept could be extended to have as many destructors as you want, 
to handle different kind of exceptions, like:

class Transaction
{
	// ...
	~this() // success
	{
		db.commit();
	}
	~this(DatabaseException e)
	{
		db.rollback();
		// some recovery code
	}
	~this(AnotherException e)
	{
		db.rollback();
		// some other stuff
	}
	~this(Exception e)
	{
		db.rollback();
	}
}

I find it a little ugly and error prone =)
If, for example, you have to use scope(exit/success/failure) you have to 
  add yourself the finalization code on every use:

Each time you want to use a transaction (for example) you have to write:
Transaction t = new Transaction(db);
scope(failure) db.rollback();
scope(exit) db.commit();
// ... code

If you forget one scope(...) db.xxx(); you hit a bug. On the other hand, 
if you could just use:
scope Transaction t = new Transaction(db);

The code is simpler, less error prone, and plus you have all the 
Transaction logic in the transaction object, not in the code that uses 
the transaction.

Opinions? Factibility?

-- 
Leandro Lucarella
Integratech S.A.
4571-5252
Nov 28 2006
parent reply Sean Kelly <sean f4.ca> writes:
Leandro Lucarella wrote:
 What do you think of adding an optional parameter (exception) to the 
 destructor, defaulting to null, to indicate the destructor was called 
 then unwinding because an exception was thrown? Then you can almost 
 forget about scope(exit/success/failure) and you have a RAII as complete 
  as Python's 'with' statement.

I don't like it, personally. It doesn't seem a good idea for a dtor to alter its behavior based on whether an exception is in flight, and exceptions should never be thrown from dtors anyway. Doing so makes writing correct code far too complicated. One thing I have done for Ares, however, is to terminate the program if one exception is thrown while another is in flight. I think DMD/Phobos does not do this currently, and instead either ignores the new exception, or substitutes it for the in-flight exception (I can't remember which). Sean
Nov 28 2006
next sibling parent reply BCS <BCS pathilink.com> writes:
Sean Kelly wrote:
 Leandro Lucarella wrote:
 What do you think of adding an optional parameter (exception) to the 
 destructor, defaulting to null, to indicate the destructor was called 
 then unwinding because an exception was thrown? Then you can almost 
 forget about scope(exit/success/failure) and you have a RAII as 
 complete  as Python's 'with' statement.

I don't like it, personally. It doesn't seem a good idea for a dtor to alter its behavior based on whether an exception is in flight, and exceptions should never be thrown from dtors anyway. Doing so makes writing correct code far too complicated. One thing I have done for Ares, however, is to terminate the program if one exception is thrown while another is in flight. I think DMD/Phobos does not do this currently, and instead either ignores the new exception, or substitutes it for the in-flight exception (I can't remember which). Sean

I think this works under DMD try { throw new Error("some stuff"); } catch(Error e) { throw new Error("more stuff\n"~e.toString); } It could be vary handy to have this ability to swap out one exception for another.
Nov 28 2006
parent reply Pragma <ericanderton yahoo.removeme.com> writes:
BCS wrote:
 Sean Kelly wrote:
 Leandro Lucarella wrote:
 What do you think of adding an optional parameter (exception) to the 
 destructor, defaulting to null, to indicate the destructor was called 
 then unwinding because an exception was thrown? Then you can almost 
 forget about scope(exit/success/failure) and you have a RAII as 
 complete  as Python's 'with' statement.

I don't like it, personally. It doesn't seem a good idea for a dtor to alter its behavior based on whether an exception is in flight, and exceptions should never be thrown from dtors anyway. Doing so makes writing correct code far too complicated. One thing I have done for Ares, however, is to terminate the program if one exception is thrown while another is in flight. I think DMD/Phobos does not do this currently, and instead either ignores the new exception, or substitutes it for the in-flight exception (I can't remember which). Sean

I think this works under DMD try { throw new Error("some stuff"); } catch(Error e) { throw new Error("more stuff\n"~e.toString); } It could be vary handy to have this ability to swap out one exception for another.

Agreed. Although I don't know if this interferes with Sean's interpretation of the spec or not. I've done this in a few places myself, and I find it quite useful. -- - EricAnderton at yahoo
Nov 28 2006
parent reply Sean Kelly <sean f4.ca> writes:
Pragma wrote:
 BCS wrote:
 Sean Kelly wrote:
 Leandro Lucarella wrote:
 What do you think of adding an optional parameter (exception) to the 
 destructor, defaulting to null, to indicate the destructor was 
 called then unwinding because an exception was thrown? Then you can 
 almost forget about scope(exit/success/failure) and you have a RAII 
 as complete  as Python's 'with' statement.

I don't like it, personally. It doesn't seem a good idea for a dtor to alter its behavior based on whether an exception is in flight, and exceptions should never be thrown from dtors anyway. Doing so makes writing correct code far too complicated. One thing I have done for Ares, however, is to terminate the program if one exception is thrown while another is in flight. I think DMD/Phobos does not do this currently, and instead either ignores the new exception, or substitutes it for the in-flight exception (I can't remember which). Sean

I think this works under DMD try { throw new Error("some stuff"); } catch(Error e) { throw new Error("more stuff\n"~e.toString); } It could be vary handy to have this ability to swap out one exception for another.

Agreed. Although I don't know if this interferes with Sean's interpretation of the spec or not. I've done this in a few places myself, and I find it quite useful.

The above example isn't the same thing. The in-flight exception is caught and a different one is rethrown. What I consider illegal is something like this: try { throw new Exception( "A" ); } finally { throw new Exception( "B" ); } What exception is in-flight after the finally block completes? And what happens to the other exception? The only correct behavior here is to terminate the application. Sean
Nov 28 2006
parent reply Sean Kelly <sean f4.ca> writes:
Sean Kelly wrote:
 Pragma wrote:
 BCS wrote:
 Sean Kelly wrote:
 Leandro Lucarella wrote:
 What do you think of adding an optional parameter (exception) to 
 the destructor, defaulting to null, to indicate the destructor was 
 called then unwinding because an exception was thrown? Then you can 
 almost forget about scope(exit/success/failure) and you have a RAII 
 as complete  as Python's 'with' statement.

I don't like it, personally. It doesn't seem a good idea for a dtor to alter its behavior based on whether an exception is in flight, and exceptions should never be thrown from dtors anyway. Doing so makes writing correct code far too complicated. One thing I have done for Ares, however, is to terminate the program if one exception is thrown while another is in flight. I think DMD/Phobos does not do this currently, and instead either ignores the new exception, or substitutes it for the in-flight exception (I can't remember which). Sean

I think this works under DMD try { throw new Error("some stuff"); } catch(Error e) { throw new Error("more stuff\n"~e.toString); } It could be vary handy to have this ability to swap out one exception for another.

Agreed. Although I don't know if this interferes with Sean's interpretation of the spec or not. I've done this in a few places myself, and I find it quite useful.

The above example isn't the same thing. The in-flight exception is caught and a different one is rethrown. What I consider illegal is something like this: try { throw new Exception( "A" ); } finally { throw new Exception( "B" ); }

By the way, the change I made will only trap exceptions thrown from an object's dtor, as it was a change to the finalizer code. The above will work just fine, with only one of the two exceptions escaping. Same as: scope(exit) throw new Exception( "B" ); throw new Exception( "A" ); I could probably be convinced that the throw from the finally block should simply be ignored, as the finally block is intended to do some simple clean-up, but the scope(exit) bit above is clearly a programming error. Throwing from an object's dtor is a lot closer to throwing from scope(exit) than finally IMO. Because while they are functionally identical, I think they are likely to be used in different ways. With a finally block, the programmer is stating that he expects an exception to be thrown and that the code should execute regardless. I'm still not certain that I like the idea of ignoring an exception from a finally block, but it bothers me less than ignoring exceptions from dtors or scope(exit)/scope(failure) blocks. Sean
Nov 28 2006
parent Sean Kelly <sean f4.ca> writes:
One more follow-up and I promise I'll stop :-)  I just remembered why I 
changed Ares in the first place.  Since objects are finalized during GC 
collections, throwing from a dtor results in completely unpredictable 
program behavior.  For example:

     {
         class MyClass
         {
             ~this()
             {
                 throw new Exception( "die" );
             }
         }
         MyClass c = new MyClass();
     }

     OtherClass c = new OtherClass(); // A

In the program above, an exception may be thrown from point A that has 
*nothing to do with an out of memory condition* and worse, it will be 
thrown only if program memory is in a state where the GC needs to 
collect to free up resources.  It's even possible that the instance of 
MyClass could have been declared and allocated in a completely different 
thread, resulting it its being passed up a call stack that was not 
written to expect such a condition.  So the presence of a GC and 
GC-called finalizers makes throwing from dtors even worse than it is in 
deterministic situations (which is still quite bad).

Also, I have found few instances where an exception really needs to be 
thrown from a dtor.  While resource cleanup operations may indeed fail, 
more often than not the documentation will say that a failure may only 
occur if the parameters are invalid.  And with proper encapsulation is 
is guaranteed not to happen.


Sean
Nov 28 2006
prev sibling parent reply Leandro Lucarella <llucarella integratech.com.ar> writes:
Sean Kelly escribió:
 Leandro Lucarella wrote:
 What do you think of adding an optional parameter (exception) to the 
 destructor, defaulting to null, to indicate the destructor was called 
 then unwinding because an exception was thrown? Then you can almost 
 forget about scope(exit/success/failure) and you have a RAII as 
 complete  as Python's 'with' statement.

I don't like it, personally. It doesn't seem a good idea for a dtor to alter its behavior based on whether an exception is in flight, and exceptions should never be thrown from dtors anyway. Doing so makes writing correct code far too complicated.

Exceptions would not be thrown from the dtor, it just executes some code if it was executed during an unwinding because an exception was raised: scope Transaction t = new Transaction(); throw Exception("ouch!"); Here the Transaction.~this(Exception) is called, but there is no Exception throwing in the dtor. I don't see why writing correct code is that complicated. And how do you address the problem of repeating error handling code and the lack of encapsulation of scope(success/failure)? Any other thoughts on this (the thread diverged a little from the original topic ;)? -- Leandro Lucarella Integratech S.A. 4571-5252
Nov 29 2006
parent reply Sean Kelly <sean f4.ca> writes:
Leandro Lucarella wrote:
 Sean Kelly escribió:
 Leandro Lucarella wrote:
 What do you think of adding an optional parameter (exception) to the 
 destructor, defaulting to null, to indicate the destructor was called 
 then unwinding because an exception was thrown? Then you can almost 
 forget about scope(exit/success/failure) and you have a RAII as 
 complete  as Python's 'with' statement.

I don't like it, personally. It doesn't seem a good idea for a dtor to alter its behavior based on whether an exception is in flight, and exceptions should never be thrown from dtors anyway. Doing so makes writing correct code far too complicated.

Exceptions would not be thrown from the dtor, it just executes some code if it was executed during an unwinding because an exception was raised: scope Transaction t = new Transaction(); throw Exception("ouch!"); Here the Transaction.~this(Exception) is called, but there is no Exception throwing in the dtor.

So are you saying you want this feature to allow Transaction to decide whether to roll-back or commit? I had assumed it was to determine whether it was "safe" for Transaction's dtor to throw an exception itself.
 I don't see why writing correct code is that complicated. And how do you 
 address the problem of repeating error handling code and the lack of 
 encapsulation of scope(success/failure)?

The lack of encapsulation doesn't bother me much, though now I see what you're getting at. I do think that having: auto scope t = new Transaction(); scope(failure) t.rollback(); // commits if not rolled back on scope exit, alternately use scope(success) t.commit(); actually aids readability a bit, at the expense of some extra code. That said, I have considered adding a routine that the user can call to determine whether an exception is in flight. Similar to the one in C++, but without all the annoying shortcomings. It would mean setting a thread-local flag or pointer in the internal exception handling code, etc. I think this is a better approach than altering dtor syntax for the same purpose, as it avoids language changes and doesn't lose any usefulness in the process.
 Any other thoughts on this (the thread diverged a little from the 
 original topic ;)?

See above :-) Sean
Nov 29 2006
parent reply Leandro Lucarella <llucarella integratech.com.ar> writes:
Sean Kelly escribió:
 I don't see why writing correct code is that complicated. And how do 
 you address the problem of repeating error handling code and the lack 
 of encapsulation of scope(success/failure)?

The lack of encapsulation doesn't bother me much, though now I see what you're getting at. I do think that having: auto scope t = new Transaction(); scope(failure) t.rollback(); // commits if not rolled back on scope exit, alternately use scope(success) t.commit(); actually aids readability a bit, at the expense of some extra code.

So you are against all RAII done in the C++ way, I guess...
 That said, I have considered adding a routine that the user can call to 
 determine whether an exception is in flight.  Similar to the one in C++, 
 but without all the annoying shortcomings.  It would mean setting a 
 thread-local flag or pointer in the internal exception handling code, 
 etc.  I think this is a better approach than altering dtor syntax for 
 the same purpose, as it avoids language changes and doesn't lose any 
 usefulness in the process.

I see this more as a hack than a clean solution (I don't say the dtor Exception parameter is heaven but I see it a little more cleaner =), but its fair enough. I didn't know C++ had a way to determine an exception is in flight...
 Any other thoughts on this (the thread diverged a little from the 
 original topic ;)?

See above :-)

Thanks, really =) -- Leandro Lucarella Integratech S.A. 4571-5252
Nov 30 2006
parent Sean Kelly <sean f4.ca> writes:
Leandro Lucarella wrote:
 Sean Kelly escribió:
 I don't see why writing correct code is that complicated. And how do 
 you address the problem of repeating error handling code and the lack 
 of encapsulation of scope(success/failure)?

The lack of encapsulation doesn't bother me much, though now I see what you're getting at. I do think that having: auto scope t = new Transaction(); scope(failure) t.rollback(); // commits if not rolled back on scope exit, alternately use scope(success) t.commit(); actually aids readability a bit, at the expense of some extra code.

So you are against all RAII done in the C++ way, I guess...

Not at all, I use it all the time :-) But I don't think the C++ method works well for situations like the above. Andrei's original series of articles on scope guards actually used transactions as their primary example for when the C++ method falls apart, and I agree with his reasoning. That said, part of his argument *was* because the C++ method for detecting in-flight exceptions isn't very reliable. Providing a fixed version in D would allow for both methods to be used--it's just a matter of making the necessary changes. All of which could be done in phobos/internal, by the way. Sean
Nov 30 2006