Thread overview
Re: DIP44: scope(class) and scope(struct)
Aug 24, 2013
H. S. Teoh
Aug 24, 2013
Ramon
Aug 24, 2013
Tobias Pankrath
Aug 25, 2013
Walter Bright
August 24, 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
August 24, 2013
Maybe I'm just plain too unexperienced or dumb but I see problems.

- What's a dtor's job in D? This is not C++ (where cleaning up often means freeing/not leaking), in D the GC saves us lots of problems. Actually, we do not even really free anything (except malloc'ed stuff) but rather hinting the runtime expressis verbis that sth. isn't needed anymore.

- scope(this) adds something new to the scope mechanism wich is per se bad and imo not justified for luxury.

- I see H.S. Teoh's point to in a way have stuff aquired or allocated in ctor kind of "registered" in dtor for proper cleanup, but that *can* be done with what we have right now.

- From what I understand the proposal implies some kind of a transactional "all or nothing" mechanism. Is a ctor and class level the right place to implement that?


@H.S. Teoh

Maybe it would helpful to explain why you think this is an *important* problem (rather than a nuisance) and more clearly define it (and why it can't be solved using what we have)?

Thanks
August 24, 2013
On Saturday, 24 August 2013 at 20:11:14 UTC, H. S. Teoh wrote:
> 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?

Like someone else already proposed: Using a wrapper type W that releases the resources in it's destructor. W.init wouldn't release anything. So by definition (if I recall the rules correctly) every new instance of C would have res3 = Resource3.init prior to it's constructor called.

Now make sure that a) res3's destructor gets called (check) b) res3's destructor may be called on Resource3.init. That would be a new rule similar to 'no internal aliasing' and c) that every constructor of C either sets res3 to a destructable value or does not touch it at all ('transactional programming').







August 25, 2013
On 8/24/2013 1:09 PM, H. S. Teoh wrote:
> 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.

If you take out automatic dtor generation, I see no difference between scope(this) and scope(failure).


>> 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.

Do you mean multiple dtors will be generated, one for each constructor?