T.init, struct destructors and invariants - should they be called?

FeepingCreature feepingcreature at gmail.de
Sun Nov 18 19:33:44 UTC 2018


On Sunday, 18 November 2018 at 15:45:51 UTC, Stanislav Blinov 
wrote:
> On Sunday, 18 November 2018 at 15:15:11 UTC, FeepingCreature 
> wrote:
>> On Sunday, 18 November 2018 at 14:54:05 UTC, Stanislav Blinov 
>> wrote:
>>> On Sunday, 18 November 2018 at 14:47:51 UTC, FeepingCreature 
>>> wrote:
>>>> On Sunday, 18 November 2018 at 14:38:09 UTC, Stanislav 
>>>> Blinov wrote:
>>>>> @safe unittest {
>>>>>     Nullable!S a; // Look ma, no assert!
>>>>> }
>>>>
>>>> a = S(new Object); // Look pa, assert!
>>>
>>> That has to do with poor implementation of that example 
>>> Nullable, not the union. opAssign should check for _hasValue.
>>
>> Right, which means you end up with moveEmplace in opAssign,
>
> No you don't :) (I know, I know, I'm such a negative 
> personality):
>
> // still rudimentary, no checks for hasElaborateDestructor
> struct Nullable(T) {
>
>     this(T val) { value = val; }
>     this(typeof(null) val) {}
>     ~this() { cleanup(); }
>
>     @property @trusted
>     ref T value() {
>         assert(_hasValue);
>         return _u.value;
>     }
>
>     @property @trusted
>     void value(T val) {
>         // when it's rebinding, there's no need to move or 
> moveEmplace,
>         // just overwrite the union
>         if (_hasValue) {
>             // this, or it could actually assign to _u.value, 
> depending on the desired semantics of Nullable
>             destroy(_u.value);
>             _u = U(val);
>         } else {
>             _u = U(val);
>             _hasValue = true;
>         }
>     }
>
>     @property
>     void value(typeof(null)) { cleanup(); }
>
>     void opAssign(T val) {
>         // arguably this should just duplicate what `value` 
> does,
>         // to avoid unnecessary copies passed around.
>         value = val;
>     }

This will not work if your type has an immutable field, by the by.

>     void opAssign(typeof(null) val) { value = val; }
>
> private:
>     union U { T value = T.init; }
>     U _u;
>     @property ref _payload() inout { return _u.value; }
>     bool _hasValue;
>
>     void cleanup() {
>         if (!_hasValue) return;
>         destroy(_u.value);
>         _hasValue = false;
>     }
> }
>
>
>> which is the current Nullable implementation.
>
> Looking at that implementation, ouch... Maybe I'm missing 
> something?..
>

> Anyway, my point is that unions *are* the tool for that 
> particular job, just like you said initially. Which has little 
> to do with the actual topic :)
> For example, I'd argue that the *actual* implementation *must* 
> `move` it's passed-by-value argument (regardless of what it 
> uses for storage), because the caller already made a required 
> copy. But that means wiping out the argument back to T.init, 
> and then we're back to square one.

Right, which is why we make a *second* copy, store it in a Union, 
and moveEmplace that.

>> Which only works because union{} essentially functions as a 
>> semi-official backdoor in the typesystem, even in @safe. Is 
>> that *really* good language design, though?
>
> That? Yes. Unions are actually useful now, unlike what they 
> were before.
>

Yeah they're useful in that they're helping us to get the 
language to defeat itself.

A destructor is something that is run when an expression goes out 
of scope. Except if that expression is stored in a union, because 
a union is a Destructor Blocker™.

No it's not! A union is a way to make multiple expressions occupy 
the same area of memory. Its definition has *nothing* to do with 
destructor blocking. The only reason that unions are destructor 
blockers are that they *can't* logically support destructors, and 
for some ~magical reason~ dlang has decided that it's @safe™ to 
let us store values with destructors in union fields anyway, 
basically for no reason other than that we decided that @safe was 
*too* @safe and we wanted it to be less @safe because if it was 
as @safe as it claimed it was, it'd be inconvenient. But to me, 
that indicates that @safe is broken, and I really don't believe 
in breaking an unrelated feature in order that I can 
coincidentally unbreak the original brokenness manually. And no, 
just because I put a fancy term on the brokenness doesn't make it 
any less broken. Unions let me take a @safe expression whose copy 
constructor has ran and avoid calling its destructor. This is 
useful because @safe would otherwise require me to run a 
destructor on some expressions whose constructor has never run. 
But two wrong designs don't make a correct design. Just because 
the building is on fire doesn't validate the decision to leave a 
giant jagged hole in the front wall.



More information about the Digitalmars-d mailing list