digitalmars.D.learn - Partially constructed objects in various languages
- bearophile (6/6) Aug 19 2013 Perhaps this thread deserves a D implementation with a small
- H. S. Teoh (36/42) Aug 23 2013 [...]
- H. S. Teoh (97/124) Aug 23 2013 [...]
Perhaps this thread deserves a D implementation with a small explanation regarding D: http://www.reddit.com/r/programming/comments/1kof0q/on_partiallyconstructed_objects/ (__ctWriteln is not yet available in D.) Bye, bearophile
Aug 19 2013
On Tue, Aug 20, 2013 at 04:55:21AM +0200, bearophile wrote:Perhaps this thread deserves a D implementation with a small explanation regarding D: http://www.reddit.com/r/programming/comments/1kof0q/on_partiallyconstructed_objects/ (__ctWriteln is not yet available in D.)[...] One issue he brought up that he didn't really address, was what to do if a ctor throws before fully initializing all fields. For example: class A { ~this() { ... } } class B { ~this() { ... } } class C { A a; B b; this() { a = new A(); if (someCondition) throw new Exception(...); b = new B(); } } The problem is, how would the compiler know to call A.~this, but not B.~this when the Exception is thrown? Also, the whole deal with partially-constructed objects seems to be related to a recent thread about encapsulating ctor parameters and objects being created first then kicked into shape (create-set-call pattern) vs. passing everything to the ctor in one go (long ctor argument list). It seems ultimately to boil down to recognizing that there's a transition point between an object's creation and its reaching a fully-valid initial state. Before this transition takes place, the object is in a partially-constructed state, and may violate class invariants. At the transition point, the ctor (or whatever's responsible for turning the object into a fully-constructed state) verifies that all constraints are met, before the object is regarded as a full instance of its class. This seems reminiscient of Perl's approach of creating an object as a normal hash, then "bless"ing it into a full-fledged class instance once it's fully initialized. T -- Long, long ago, the ancient Chinese invented a device that lets them see through walls. It was called the "window".
Aug 23 2013
On Fri, Aug 23, 2013 at 01:39:05PM -0700, H. S. Teoh wrote:On Tue, Aug 20, 2013 at 04:55:21AM +0200, bearophile wrote:[...] Actually, a better example might be: class C { Resource1 res1; Resource2 res2; Resource3 res3; this() { res1 = acquireResource!1(); res2 = acquireResource!2(); if (someCondition) throw new Exception(...); res3 = acquireResource!3(); } ~this() { res3.release(); res2.release(); res1.release(); } } The problem is, once the Exception is thrown, what do you do? You can't call the dtor (res3.release() is invalid because res3 hasn't been acquired yet), but you do need to cleanup (res1 and res2 must be released). This is one area where the conventional ctor/dtor scheme does not work very well. Essentially you have to uglify your code by making Resource1, Resource2, Resource3 nullable, then in the dtor you check for null before releasing it: class C { Nullable!Resource1 res1; ... // etc. this() { /* as before */ } ... ~this() { if (res3 !is null) res3.release(); if (res2 !is null) res2.release(); if (res1 !is null) res1.release(); } } Or, as Adam Ruppe once did in his Terminal code, you implement a kind of class-wide scope guard: class C { void delegate()[] cleanupFuncs; Resource1 res1; Resource2 res2; Resource3 res3; this() { res1 = acquireResource!1(); cleanupFuncs ~= { res1.release(); }; res2 = acquireResource!2(); cleanupFuncs ~= { res2.release(); }; res3 = acquireResource!3(); cleanupFuncs ~= { res3.release(); }; } ~this { foreach_reverse(f; cleanupFuncs) { f(); } } } This has the nice property that, like scope guards, the cleanup sits next to the initialization, so it's less prone to mistakes (e.g. ctor gets changed to acquire a 4th resource, but the author forgot to add a corresponding release() to the dtor). It also has the property that only resources that are actually acquired will get cleaned up, making the dtor safe to call if the ctor throws. So for example, if acquireResource!3() throws an Exception, then only res1 and res2's cleanup delegates are registered, so the dtor will only clean up res1 and res2 and leave res3 untouched, which is correct since res3 was never initialized. Hmm. On second thoughts, maybe we should have language-support for this kind of class-wide (or struct-wide) scope guard, and deprecate dtors, which are unreliable anyways due to the GC. So maybe something like: class C { Resource1 res1; Resource2 res2; Resource3 res3; this() { res1 = acquireResource!1(); scope(class) res1.release(); res2 = acquireResource!2(); scope(class) res2.release(); res3 = acquireResource!3(); scope(class) res3.release(); } // no explicit dtor necessary, compiler supplies one for you // composed of all the scope(class) blocks encountered // in the ctor. } I actually like this idea a lot! It addresses the partially-constructed object problem in a nice way, and also extend scope guards to be applicable in more situations. (It feels like such a pity that a clever feature like scope guards would be used so little in D.) T -- Always remember that you are unique. Just like everybody else. -- despair.comPerhaps this thread deserves a D implementation with a small explanation regarding D: http://www.reddit.com/r/programming/comments/1kof0q/on_partiallyconstructed_objects/ (__ctWriteln is not yet available in D.)[...] One issue he brought up that he didn't really address, was what to do if a ctor throws before fully initializing all fields. For example: class A { ~this() { ... } } class B { ~this() { ... } } class C { A a; B b; this() { a = new A(); if (someCondition) throw new Exception(...); b = new B(); } } The problem is, how would the compiler know to call A.~this, but not B.~this when the Exception is thrown?
Aug 23 2013