H. S. Teoh 
| 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
|