www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - DIP44: scope(class) and scope(struct)

reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
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

[*] The partially-constructed object problem is when you have a class
(or struct) that must acquire some number of external resources, usually
to set them as member fields, but before the ctor is able to initialize
all of these fields, an Exception is thrown. Now the object is in a
partially-constructed state: some member fields have been initialized,
and need to be destructed in order to release the associated external
resources, but other fields are still uninitialized so should not be
destructed. This leads to the problem of, how do we clean up in this
situation? We can't call the dtor -- the dtor assumes *all* fields have
been set and will wrongly try to release resources that haven't been
acquired yet. But we can't ignore the issue either -- the resources that
*have* been required need to be released somehow. This DIP proposes a
nice solution to this problem that fits in very well with the existing
scope guards in D.

Destroy! ;-)


T

-- 
Only boring people get bored. -- JM
Aug 23 2013
next sibling parent "Andrej Mitrovic" <andrej.mitrovich gmail.com> writes:
On Saturday, 24 August 2013 at 00:45:46 UTC, H. S. Teoh wrote:
 Destroy! ;-)

I won't destroy but I'll say avoid the struct/class keywords and use `scope(this)` instead, it looks nicer and will work easier in generic code.
Aug 23 2013
prev sibling next sibling parent "Andrej Mitrovic" <andrej.mitrovich gmail.com> writes:
On Saturday, 24 August 2013 at 00:48:46 UTC, Andrej Mitrovic 
wrote:
 On Saturday, 24 August 2013 at 00:45:46 UTC, H. S. Teoh wrote:
 Destroy! ;-)

I won't destroy but I'll say avoid the struct/class keywords and use `scope(this)` instead, it looks nicer and will work easier in generic code.

Perhaps scope(~this) is actually more appropriate.
Aug 23 2013
prev sibling next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Sat, Aug 24, 2013 at 03:06:39AM +0200, Andrej Mitrovic wrote:
 On Saturday, 24 August 2013 at 00:48:46 UTC, Andrej Mitrovic wrote:
On Saturday, 24 August 2013 at 00:45:46 UTC, H. S. Teoh wrote:
Destroy! ;-)

I won't destroy but I'll say avoid the struct/class keywords and use `scope(this)` instead, it looks nicer and will work easier in generic code.

Perhaps scope(~this) is actually more appropriate.

Hmm. You do have a point. But I'm not sure I like breaking the pattern of a single identifier inside scope(). I still think scope(this) looks nicer and is nicer to type as well. And also reads like "scope of this object", which kinda conveys the intent. But anyway, we can bikeshed over the exact syntax later. What of the proposal itself? Does it make any sense? Any obvious glaring flaws? Also, it just occurred to me that Adam's manual implementation can be made to handle partial object construction as well, by using scope(failure) in the ctor: struct S { void delegate()[] cleanupFuncs; Resource1 res1; // ... this() { // assuming the dtor isn't invoked if the ctor // throws. scope(failure) cleanup(); res1 = acquireResource(); cleanupFuncs ~= { res1.release(); } ... } void cleanup() { foreach_reverse (f; cleanupFuncs) f(); } ~this() { cleanup(); } } So scope(this) could simply be lowered to the above code, perhaps with some compiler low-level handling for the potential problem with calling a method from ~this(), which I think can be a problem in the current implementation of dtors? (IIRC 'this' may be invalid when ~this() is invoked, or something dangerous like that?) In any case, the above code has a lot of boilerplate which scope(this) would eliminate, besides expanding the concept of scope guards to object lifetimes. T -- Unix was not designed to stop people from doing stupid things, because that would also stop them from doing clever things. -- Doug Gwyn
Aug 23 2013
prev sibling next sibling parent "Ramon" <spam thanks.no> writes:
I don't want to be picky but I feel one shouldn't pack too much 
into one thing and in a way break the logic flow and possibly 
introduce limitations.

The basic rule is to clean up in the destructor what has been 
allocated/aquired in the constructor - and this shouldn't be 
interrupted but stay that way.
What you want to address, if I got it right (If not, I apologize 
for my interruption) is the problem of incoherent state, i.e. 
(relating to your example) to have res1 and res2 aquired but not 
res3.
In my (possibly strongly limited) understanding scope(failure) is 
the right approach.

Having a mechanism (as suggested) that also takes care of the 
dtor's job breaks the normal logic and might create new problems 
or limitations by implicitly putting a dtor job into a ctor 
mechanism.

What, for instance, if I aquire 10 resources in ctor and during 
normal programm execution cleanup 7 of them, so only some are 
left for dtor?


If scope(failure) does already work in ctor, just keep it that 
way. If not,HS Teoh's idea to make it work is great. But that 
about it. Don't add more functionality in it. Normal ctor/dtor 
working should be kept and so should scope(). Whatever user wants 
beyond that can be coded by him. Don't break major flow for some 
luxury (having scope in ctor isn't luxury, having it reach into 
dtor, too up to the point of making dtor superflous is).

scope is a brilliant feature. But I feel we shouldn't have it do 
"miracles", even less behind curtains.

In case what I said is just plain stupid and I should have shut 
up as newbie, I apologize and won't disturb any more.
Aug 23 2013
prev sibling next sibling parent "Meta" <jared771 gmail.com> writes:
"With this extension to scope guards, class and struct 
destructors will practically not be needed anymore, since 
scope(this) will take care of cleaning up everything."

I can't think of an example off the top of my head, but is it 
really okay to conflate destruction due to an error during 
construction, and destruction over the regular course of a 
struct's usage? What if one instance requires different code from 
the other? Maybe require that scope(this) statements *only* be 
run if there is an error during construction, while just the 
destructor will be run normally?
Aug 23 2013
prev sibling next sibling parent Piotr Szturmaj <bncrbme jadamspam.pl> writes:
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

 Destroy! ;-)

I see some possible implementation problem. Using a scope guard in a single function is straightforward because code is executed in the same scope. However, in your proposal scope guard is split to two different functions/scopes. This may require copying local stack variables and storing them somewhere if they're referred to in the scope(this). For example: ResourceManager resManager; class C { Resource res; this(int resourceId) { res = resManager.acquireResource(resourceId); scope(this) resManager.releaseResource(resourceId); } } Here the resourceId value must be stored somewhere so it can be used later, when the object is destructed. I'm thinking of two possible "solutions" to this problem: 1. disallow referring to local variables in scope(this). In this case resourceId should be stored in a field of class C or in the Resource itself (it may not always be allowed). 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.
Aug 23 2013
prev sibling next sibling parent "Simen Kjaeraas" <simen.kjaras gmail.com> writes:
On Sat, 24 Aug 2013 03:34:32 +0200, Ramon <spam thanks.no> wrote:

 What, for instance, if I aquire 10 resources in ctor and during normal  
 programm execution cleanup 7 of them, so only some are left for dtor?

Then they don't have the same lifetime as the object, and scope(this) would be the wrong tool for the job. -- Simen
Aug 24 2013
prev sibling next sibling parent Artur Skawina <art.08.09 gmail.com> writes:
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. 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. 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. artur
Aug 24 2013
prev sibling next sibling parent "deadalnix" <deadalnix gmail.com> writes:
On Saturday, 24 August 2013 at 00:45:46 UTC, 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

 [*] The partially-constructed object problem is when you have a 
 class
 (or struct) that must acquire some number of external 
 resources, usually
 to set them as member fields, but before the ctor is able to 
 initialize
 all of these fields, an Exception is thrown. Now the object is 
 in a
 partially-constructed state: some member fields have been 
 initialized,
 and need to be destructed in order to release the associated 
 external
 resources, but other fields are still uninitialized so should 
 not be
 destructed. This leads to the problem of, how do we clean up in 
 this
 situation? We can't call the dtor -- the dtor assumes *all* 
 fields have
 been set and will wrongly try to release resources that haven't 
 been
 acquired yet. But we can't ignore the issue either -- the 
 resources that
 *have* been required need to be released somehow. This DIP 
 proposes a
 nice solution to this problem that fits in very well with the 
 existing
 scope guards in D.

 Destroy! ;-)


 T

I like the feature. I wouldn't say this is the most important thing to add here, the same can be achieved with scope(failure) and destructor.
Aug 24 2013
prev sibling next sibling parent "Ramon" <spam thanks.no> writes:
On Saturday, 24 August 2013 at 08:34:10 UTC, Simen Kjaeraas wrote:
 On Sat, 24 Aug 2013 03:34:32 +0200, Ramon <spam thanks.no> 
 wrote:

 What, for instance, if I aquire 10 resources in ctor and 
 during normal programm execution cleanup 7 of them, so only 
 some are left for dtor?

Then they don't have the same lifetime as the object, and scope(this) would be the wrong tool for the job.

Understood. But there *are* objects with different lifetimes. If, for instance, I aquire some "nice" objects (GC'd, properly through D) - and - some "dirty" malloced stuff (not talking about externals). Thinking about it I also dislike a special thing that also feels and shows a special thing (namely "this" rather than e.g. "failure"). "Holy rule": No special cases if any possible but everything in a consistent way. If there are special cases, somehow make them not look like it; try to make them look consistent. The scope mechanism deals with success, with failure and with exit. That's great, that's sufficient. That's how it should at least *look* like in ctors, too. Another inconsistency: If this mechanism is to "guard" on class level, it should be there, at class level and not in a function like entity (albeit the rather special "function" this). Maybe I'm overly shy but I feel that logical consistency is a major issue to be a good language and for safety, too.
Aug 24 2013
prev sibling next sibling parent "Tobias Pankrath" <tobias pankrath.net> writes:
On Saturday, 24 August 2013 at 00:45:46 UTC, 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:

[snip] What about a language rule, that for every type T destroy(t) must be valid if t == T.init? 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.
Aug 24 2013
prev sibling next sibling parent reply Dmitry Olshansky <dmitry.olsh gmail.com> writes:
24-Aug-2013 04:44, H. S. Teoh пишет:
 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

 Destroy! ;-)

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(); } } There are many more alternatives on how to auto-generate RAII helpers. The point is we can easily do so, after all our compile-time kung-fu is strong. -- Dmitry Olshansky
Aug 24 2013
next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Sat, Aug 24, 2013 at 01:09:44PM -0700, H. S. Teoh wrote:
[...]
 On Sat, Aug 24, 2013 at 12:27:37PM -0700, Walter Bright wrote:

 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.

Argh, that was poorly worded. What I mean is this: struct S { this(int) { scope(this) writeln("A"); } this(float) { scope(this) writeln("B"); } } void fun1() { auto s = S(1); } // prints "A" void fun2() { auto s = S(1.0); } // prints "B" T -- Dogs have owners ... cats have staff. -- Krista Casada
Aug 24 2013
prev sibling parent Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
On 8/24/13 10:31 AM, Dmitry Olshansky wrote:
 24-Aug-2013 04:44, H. S. Teoh пишет:
 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

 Destroy! ;-)

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.

Agreed. DIP44 supports an idiom of handling multiple unprotected resources within the same object, which should be infrequent and generally unrecommended. Andrei
Aug 24 2013
prev sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 8/23/2013 5:44 PM, 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

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? 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?
Aug 24 2013
parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Sat, Aug 24, 2013 at 06:50:11PM -0700, Walter Bright wrote:
 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?

No. Given this code: class C { this(int) { scope(this) writeln("A1"); scope(this) writeln("A2"); } this(float) { scope(this) writeln("B1"); scope(this) writeln("B2"); } } The lowered code looks something like this: class C { this(int) { scope(failure) __cleanup(); // scope(this) writeln("A1"); // Translated into: __cleanups ~= { writeln("A1"); }; // scope(this) writeln("A2"); // Translated into: __cleanups ~= { writeln("A2"); }; } this(float) { scope(failure) __cleanup(); // scope(this) writeln("B1"); // Translated into: __cleanups ~= { writeln("B1"); }; // scope(this) writeln("B2"); // Translated into: __cleanups ~= { writeln("B2"); }; } void delegate()[] __cleanups; void __cleanup() { foreach_reverse (f; __cleanups) f(); } ~this() { __cleanup(); } } So, there is only one dtor. But it automatically takes handles triggering the scope(this) statements of only the ctor that was actually used for creating the object. And if the ctor didn't successfully complete, it only triggers the scope(this) statements that have been encountered up to that point. Of course, the above lowered code is just to illustrate how it works. The actual implementation can be optimized by the compiler. E.g., if there is only one ctor and the sequence of scope(this) can be statically determined, then the cleanup statements can be put into the dtor directly without incurring the overhead of allocating the __cleanups array. If the cleanups don't need closure over ctor local variables, then they can just be function ptrs, not delegates. Only when you're doing something complicated do you actually need an array of delegates, which should be relatively rare. T -- Political correctness: socially-sanctioned hypocrisy.
Aug 24 2013