November 18, 2018 Re: T.init, struct destructors and invariants - should they be called? | ||||
---|---|---|---|---|
| ||||
Posted in reply to Stanislav Blinov | 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!
|
November 18, 2018 Re: T.init, struct destructors and invariants - should they be called? | ||||
---|---|---|---|---|
| ||||
Posted in reply to FeepingCreature | 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.
|
November 18, 2018 Re: T.init, struct destructors and invariants - should they be called? | ||||
---|---|---|---|---|
| ||||
Posted in reply to FeepingCreature | 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!
...i.e. it should be:
// ...
@property @trusted
void value(T val) {
if (!_hasValue) {
import std.conv : emplace;
auto addr = () @trusted { return &_payload(); } ();
emplace(addr, val);
_hasValue = true;
} else
_payload = val;
}
// ...
since otherwise that calls opAssign. Unless you'd propose to not call invariants for that one too :*)
|
November 18, 2018 Re: T.init, struct destructors and invariants - should they be called? | ||||
---|---|---|---|---|
| ||||
Posted in reply to Stanislav Blinov | 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, which is the current Nullable implementation. Which only works because union{} essentially functions as a semi-official backdoor in the typesystem, even in @safe. https://run.dlang.io/is/YAnVBV Is that *really* good language design, though? |
November 18, 2018 Re: T.init, struct destructors and invariants - should they be called? | ||||
---|---|---|---|---|
| ||||
Posted in reply to FeepingCreature | 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; } 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?.. > 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. 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. |
November 18, 2018 Re: T.init, struct destructors and invariants - should they be called? | ||||
---|---|---|---|---|
| ||||
Posted in reply to Stanislav Blinov | 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. |
November 19, 2018 Re: T.init, struct destructors and invariants - should they be called? | ||||
---|---|---|---|---|
| ||||
Posted in reply to FeepingCreature | On Sunday, 18 November 2018 at 19:33:44 UTC, FeepingCreature wrote: > On Sunday, 18 November 2018 at 15:45:51 UTC, Stanislav Blinov wrote: >> 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. You misunderstood what I said, and as you saw in my last example you don't need a moveEmplace for the current implementation. What I'm saying is, the actual implementation *should* be doing this: void opAssign(T rhs) { // ... move(rhs, myOwnStorage); // ... } T, for all I know, could be non-copyable, i.e. a Unique. That way the onus of making an instance falls solely on the caller, while Nullalbe would never call or deal with any copy constructors. Recall that D "frowns upon" self-referencing types. Although the language can't statically disallow them, it's free to assume they don't exist. Therefore, moving values around without calling their copy ctors should be acceptable. That's not a `union` problem. If you do it like this where T is your S, you'll get an assert due to invariant. I.e. that is the subject problem :) > ...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. Again, that is not a `union` problem, that's a destructor+invariant problem. Types in .init state should be destructible, period: S[] a; // can't do this: a = new S[10]; // but still can do this: a.length = 10; |
November 19, 2018 Re: T.init, struct destructors and invariants - should they be called? | ||||
---|---|---|---|---|
| ||||
Posted in reply to Stanislav Blinov | On Monday, 19 November 2018 at 01:46:34 UTC, Stanislav Blinov wrote: > Again, that is not a `union` problem, that's a destructor+invariant problem. Types in .init state should be destructible, period: > > S[] a; > // can't do this: > a = new S[10]; > // but still can do this: > a.length = 10; Fair enough, I agree with that, it's just been a bit of an uphill struggle to get people to agree that requiring T.init to pass the invariants makes struct invariants mostly useless. If you can get them to agree to a solution that doesn't nerf struct invariants into the ground, then be my guest - there's a DMD PR that could be resurrected, https://github.com/dlang/dmd/pull/8462 , or a better solution found. I just really don't want to have to take our codebase back to classes for domain values, or comment out all the invariants we painstakingly added. |
November 25, 2018 Re: T.init, struct destructors and invariants - should they be called? | ||||
---|---|---|---|---|
| ||||
Posted in reply to FeepingCreature | Ping. |
November 25, 2018 Re: T.init, struct destructors and invariants - should they be called? | ||||
---|---|---|---|---|
| ||||
Posted in reply to FeepingCreature | On Sunday, 25 November 2018 at 16:05:11 UTC, FeepingCreature wrote:
> Ping.
Pong on GH.
|
Copyright © 1999-2021 by the D Language Foundation