What is going on here?

w0rp via Digitalmars-d digitalmars-d at puremagic.com
Sun Mar 8 06:56:32 PDT 2015


On Sunday, 8 March 2015 at 07:02:48 UTC, Shachar Shemesh wrote:
> On 07/03/15 23:44, w0rp wrote:
>> Why not handle the scope(failure) cases in the constructor 
>> itself?
>>
> Because this breaks encapsulation.
>
> struct SomeStruct {
> 	SomeOtherStruct a;
> 	MoreStruct b;
>
> 	this(int c) {
> 		this.a = SomeOtherStruct(c);
> 		this.b = MoreStruct(c+1);
> 	}
> }
>
> This code, as written, reasonable though it may seem, does not 
> work with D's current implementation. If MoreStruct's 
> constructor throws, a will be left with dangling resources. Or 
> not. It really depends on how SomeOtherStruct is implemented.
>
> Which is exactly my point. For things to be kept sane, each 
> struct must be able to completely control its own resources. 
> Once we accept that, however, a simple look at your example 
> shows there is a much cleaner way of doing it:
>
>> struct SomeStruct {
>>     SmartPtr ptr;
>>     SmartPtr ptr2;
>>
>>     this() {
>>         ptr = malloc(...);
>>         // No longer necessary: scope(failure) free(ptr);
>>
>>         /* Whatever follows, possibly throwing. */
>>
>>         ptr2 = malloc(...);
>>         // No longer necessary: scope(failure) free(ptr2);
>>     }
>>
>>     // No longer necessary: there is nothing left for us to 
>> destruct: ~this()
>> }
>
> This also avoids the bugs your implementation has, where you 
> failed to override this(this) and opAssign, and thus you both 
> are leaking some memory AND double freeing another. If SmartPtr 
> is implemented correctly, the above, as is, is bug free.
>
> Which is precisely why RAII are so great. They reduce the 
> amount of code you need to write in order to be bug-free. You 
> contain the tough corner cases in the RAII implementation, 
> leaving everyone else to just write their code in peace.
>
> Which is why I think problems such as this are a real setback.
>
> Shachar

I only wrote enough to show the really important part. You 
probably want a resource to not be copyable at all, only 
moveable, so I'd probably write @disable this(this) and probably 
also @disable this(). As for a struct with a throwing desctructor 
creating other structs with throwing destructors, my two rules 
apply exactly as before. If the struct's constructor throws, 
release the allocated resources manually with scope(failure) 
cases. Sure encapsulation will be broken, but at the moment you 
can do no better.

The only way we can improve on this is to have the destructors of 
each field called, but not the destructor itself, exactly as C++ 
does it. One complication, as others have mentioned, is that 
construction of each field cannot be stated explictly away from 
the actual constructor, so that might cause some issues.

I'll note that following the rules with non-copyable objects, you 
won't ever double-free any resources. If a constructor of some 
object in the constructor throws, its destructor won't be called 
ever, as you won't hit the following explicit scope(failure) 
case. You won't have a dangling resource, because the previous 
objects with scope(failure) cases will be destroyed, but again 
not double-freed because the struct's destructor won't be run.

So if you want something to work as of right now, do that.


More information about the Digitalmars-d mailing list