DIP44: scope(class) and scope(struct)

H. S. Teoh hsteoh at quickfur.ath.cx
Sat Aug 24 13:09:44 PDT 2013


On Sat, Aug 24, 2013 at 01:01:12PM +0200, Artur Skawina wrote:
> On 08/24/13 08:30, Piotr Szturmaj wrote:
> > H. S. Teoh wrote:
> >> I've written up a proposal to solve the partially-constructed
> >> object problem[*] in D in a very nice way by extending scope
> >> guards:
> >>
> >>     http://wiki.dlang.org/DIP44
> 
> > 2. allow referring to local variables and create the closure. This
> > causes additional memory allocation and also reference to the
> > closure must be stored somewhere, perhaps in the class hidden field.
> > Of course, this is a "no go", I'm writing this here for comparison.
> 
> That is what he's actually proposing. And, yes, it's not a good idea.
> Implementing it via runtime delegates, implicit captures/allocs and
> extra hidden fields inside aggregates would work, but the cost is too
> high. It's also not much of an improvement over manually registering
> and calling the delegates. Defining what happens when a ctor fails
> would be a good idea, having a cleanup method which defaults to
> `~this`, but can be overridden could help too.

The issue with that is that the initialization code and the cleanup code
has to be separated, potentially by a lot of unrelated stuff in between.
The genius of scope guards is that initialization and cleanup is written
in one place even though they actually happen in different places, so
it's very unlikely you will forget to cleanup correctly.


> There are other problems with that DIP, like making it harder to see
> what's actually going on, by splitting the dtor code and having it
> interleaved with another separate flow.

I think it's unhelpful to conflate scope(this) with dtors. Yes there is
some overlap, but if you treat them separately, then there is no
problem (assuming that a dtor is actually necessary).


> It *is* possible to implement a similar solution without any RT cost,
> but it would need:
> a) flow analysis - to figure out the cleanup order, which might not be 
>     statically known (these cases have to be disallowed)
> b) a different approach for specifying the cleanup code, so that 
>     implicit capturing of ctor state doesn't happen and it's not
>     necessary to read the complete body of every ctor just to find
>     out what a dtor does.

I think that's an unhelpful way of thinking about it. What about we
think of it this way: the ctor is acquiring X number of resources, and
by wrapping the resource-releasing code in scope(this), we guarantee
that these resources will be correctly released. Basically, scope(this)
will take care of invoking the release code whenever the object's
lifetime is over, whether it's unsuccessful construction, or
destruction.

It's just like saying scope guards are useless because it's equivalent
to a try-catch block anyway (and in fact, that's how the front end
implements scope guards). One may even argue scope guards are bad
because the cleanup code is sprinkled everywhere rather than collected
in one place. But it's not really about whether it's equivalent to
another language construct; it's about better code maintainability.
Separating the code that initializes something from the code that cleans
up something makes it harder to maintain, and more error-prone (e.g.
initialize 9 things, forget to clean up one of them). Keeping them
together in the same place makes code correctness clearer.


On Sat, Aug 24, 2013 at 02:48:53PM +0200, Tobias Pankrath wrote:
[...]
> Couldn't this problem be solved by using RAII and destructors? You
> would (only) need to make sure that every member is either correctly
> initialised or T.init.

How would you use RAII to solve this problem? If I have a class:

	class C {
		Resource1 res1;
		Resource2 res2;
		Resource3 res3;
		this() {
			...
		}
	}

How would you write the ctor with RAII such that if it successfully
inits res1 and res2, but throws before it inits res3, then only res1 and
res2 will be cleaned up?


On Sat, Aug 24, 2013 at 09:31:52PM +0400, Dmitry Olshansky wrote:
[...]
> Instead of introducing extra mess into an already tricky ctor/dtor
> situation. (Just peek at past issues with dtors not being called,
> being called at wrong time, etc.)
> 
> I'd say just go RAII in bits and pieces. Unlike scope, there it
> works just fine as it has the right kind of lifetime from the get
> go. In function scope (where scope(exit/success/failure) shines)
> RAII actually sucks as it may prolong the object lifetime I you are
> not careful to tightly wrap it into { }.
> 
> Start with this:
> 
> class C {
>     Resource1 res1;
>     Resource2 res2;
>     Resource3 res3;
> 
>     this() {
>         res1 = acquireResource!1();
>         res2 = acquireResource!2();
>         res3 = acquireResource!3();
>     }
> 
>     ~this() {
>         res3.release();
>         res2.release();
>         res1.release();
>     }
> }
> 
> Write a helper once:
> 
> struct Handler(alias acquire, alias release)
> {
> 	alias Resource = typeof(acquire());
> 	Resource resource;
> 	this(int dummy) //OMG when 0-argument ctor becomes usable?
> 	{
> 		resource = acquire();
> 	}
> 
> 	static auto acquire()
> 	{
> 		return Handler(0); //ditto
> 	}
> 
> 	~this()
> 	{
> 		release(resource);
> 	}
> 	alias this resource;
> }
> 
> 
> Then:
> 
> class C{
>     Handler!(acquireResource!1, (r){ r.release(); }) res1;
>     Handler!(acquireResource!2, (r){ r.release(); }) res2;
>     Handler!(acquireResource!3, (r){ r.release(); }) res3;
>     this(){
> 	res1 = typeof(res1).acquire();
> 	res2 = typeof(res2).acquire();
> 	res3 = typeof(res3).acquire();
>     }
> }

I don't see how code solves the problem. Suppose this() throws an
Exception after res1 and res2 have been initialized, but before res3 is
uninitialized. Now what? How would the language know to only clean up
res1 and res2, but not res3? How would the language know to only invoke
the dtors of res1 and res2, but not res3?


On Sat, Aug 24, 2013 at 12:27:37PM -0700, Walter Bright wrote:
[...]
> Not a bad idea, but it has some issues:
> 
> 1. scope(failure) takes care of most of it already
> 
> 2. I'm not sure this is a problem that needs solving, as the DIP
> points out, these issues are already easily dealt with. We should be
> conservative about adding more syntax.
> 
> 3. What if the destructor needs to do more than just unwind the
> transactions? Where does that code fit in?

I think it's unhelpful to conflate scope(this) with dtors. They are
related, but -- and I guess I was a bit too strong about saying dtors
are redundant -- if we allow both, then scope(this) can be reserved for
transactions, and you can still put code in ~this() to do non-trivial
cleanups.


> 4. The worst issue is the DIP assumes there is only one constructor,
> from which the destructor is inferred. What if there is more than
> one constructor?

This is not a problem. If there is more than one constructor, then only
those scope(this) statements in the ctor that were actually encountered
will trigger when the object reaches the end of its lifetime.  You
already have to do this anyway, since if the ctor throws an Exception
before completely constructing the object, only those scope(this)
statements that have been encountered up to that point will be
triggered, not all of them. Otherwise, you'd still have the
partially-initialized object problem.


T

-- 
Without geometry, life would be pointless. -- VS


More information about the Digitalmars-d mailing list