Fix issue 19065: don't check struct invariant before destructor runs.#8462
Conversation
|
Thanks for your pull request and interest in making D better, @FeepingCreature! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please see CONTRIBUTING.md for more information. If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment. Bugzilla references
Testing this PR locallyIf you don't have a local development environment setup, you can use Digger to test this PR: dub fetch digger
dub run digger -- build "master + dmd#8462" |
😄 Joke aside, I think this is the most compact change that makes |
|
This is an enhancement, not a bug report. See 19065. Needs a compelling rationale. |
|
See also forum thread for attempt at running discussion. |
|
Would it help if I made a DIP? |
|
Yes, for a DIP there's a formal layout and procedure which will ensure a decision/reply. |
|
Yeah I'll try to get generic Nullable merged and probably work around this weird design decision in userland, As One Does In D. Getting ready to write a whole lot of |
|
BTW as you are looking into D's nullable Lukas (from the C++ Munich Meetup) shared a very interesting finding with me. |
|
Okay, I think part of it is that for some reason ldc decides it's a good idea to fill the upper 7 bytes of Nullable (not taken up by the bool) with zeroes manually using a memcpy, and LLVM can't handle this. After that I don't know. I think possibly because Optional is standardized (in Rust as well), clang++ is smart enough to decide that if an optional is null, it doesn't matter what's in the value field. I don't know if D can compete with tricks like that; we'd probably need an Optional type in the compiler. edit: Note the asm looks a lot better if you inline Nullable. |
|
I don't like this change. I mean, if this is what you want, you can always do that in your invariant: invariant {
assert(this == typeof(this).init || your actual check here);
} |
|
On reflection, this is probably not the way to go. The thing that I actually need is a way to decouple struct value lifetime from struct variable lifetime. This is necessary so you can implement container types containing arbitrary types in a way that doesn't end up with the container types crashing when they go out of scope and D tries to destruct the variables that they contain, which may not even be initialized to values but to, say, |
|
@FeepingCreature This is not the problem here. Destructor of T should always be able to handle T.init. One of @WalterBright's argument against struct default constructor is that not having it will force the programmer to consider T.init a valid value of T. |
You can I mean, if we're deciding that structs aren't allowed to hold values that mustn't be null that indicates a pretty hefty step backwards for the language for me, since it'll force us to go back to classes for the majority of domain values, resulting in a massive tree of tiny values connected by a sprawling web of references, with all the GC overhead that implies. My impression of |
UplinkCoder
left a comment
There was a problem hiding this comment.
I like this PR.
It's elegantly written and has a compelling rational behind it.
|
@UplinkCoder It doesn't compel me. @FeepingCreature If you want a struct that never holds a null value. Than its .init can't be null either. Otherwise T.init will hold a null value and break your assumption that T is never null. T.init is always assumed to be a valid value (according to @WalterBright) In fact doing what you are suggesting in this PR in fact semi solidifies that T.init is a valid value: you made it always valid for destructor calls. User has no way to prevent the variable from holding T.init if no other methods than the destructor is called. |
That doesn't work. You can't give a struct a default value of an object that only makes sense at runtime. To say that its init can't be null is to say I can't have an invariant that a struct must hold a valid object; that is, I can't put an object reference in a struct and be assured it's not null. With that restriction,
Yes, that's the point. I'm defining struct valid values to be {a value that passes the invariant, T.init}, then restricting the methods defined to be callable on an "extended valid value" to be only the destructor. Again, the only reason for this is that the destructor is the only struct method that is always, inevitably called and cannot be skipped. (Discounting the Semantically, I'm separating "struct values" from "struct variables". I'm saying, "any struct variable can, but doesn't have to, hold a struct value; however, it must be either holding a struct value or init; and if it holds init, only its destructor may be called." This is consistent with how it works right now - in fact, it's more consistent than it works right now - because the destructor is called lexically and has no relation to struct construction. I understand that this is subtle, but you need to understand that barring hacks, it's impossible to write a (pointerless) |
|
@FeepingCreature If so, instead of not check invariants at all before destructor, you should check if the variable is T.init, and only skip invariants in that case. |
|
@yshui I agree that implementation would be superior, but I'm not good enough at DMD internals to implement it. |
|
Also
I don't know what do you mean by "hacks". Using And I want to backpaddle a bit on my argument. This change forces the destructor to handle All in all I just dislike changes that adds more "subtlety" to language. Especially when the added "subtlety" can be done equally well in the library. (In that case, Edit: Just noticed in the forum thread you discussed how union doesn't work. I don't quite see why? |
I don't understand how you can call that "not a hack". It's a massive hack. Unions cannot support destructors because they don't know what's in them, and that's inherent to unions, sure, but the expected answer to that isn't "so I guess unions can't call destructors", the expected and proper answer is "types with destructors cannot be put in unions." It is mere luck that D does not work that way. My first preferred solution would be a language level way to mark a variable as "manually destroyed" (
I don't remember that, link please? In any case, my |
|
@FeepingCreature There're a lot of things in D that just "happen to work". And because of that, we already have a way to create "manually destroyed" variables, that is, by using unions. So a language change to add another way to do that is doing to face resistance. (BTW I don't see why only manually destroyed variable can be initialized by |
Yes and if we rely on this too much, we'll never be able to get this unfortunate design decision fixed. It'll just be with us forever, because it'll be too much work to change. So I'm conflicted.
Because language-managed variables should require that a constructor call be matched up with a destructor call because that's what constructor/destructor means - one makes a value, the other unmakes it. The proper way to create a variable is that its value is created by a constructor call and destroyed by a destructor call. The only case where |
|
@FeepingCreature First off, constructor is definitely not the only way to "make" a value, and people are doing that all the time. Like, what about POD structs? |
|
POD structs don't have destructors either. Besides, you can just consider them to have "default constructors/destructors", which they actually will if you put a type with a constructor or destructor in them, and if you don't, well, how would you know? Lots of types don't run into this issue. But when they do, it would be good if the language was internally consistent. |
| @safe void main() | ||
| { | ||
| S s = S.init; | ||
| } |
There was a problem hiding this comment.
Will the opposite fail? i.e.
struct S {
bool value = false;
invariant {
assert(value == false);
}
@disable this();
}
void main() {
S s = S.init;
s.value = true;
}
I ask because I wonder if the check to see if the invariant needs to be called before the destructor depends on a by value comparison of this and T.init or an address?
If it's a by value comparison then will it work for classes where this == other compares addresses?
There was a problem hiding this comment.
There is no such check. That was something that'd been proposed, but I don't know how to write it.
I'm going to close this PR. It doesn't do anything good or worthwhile, and I found a better way. (Unions skip destructors, apparently intentionally??)
|
Closed because union hack lets us work around it. |
|
I'd rather you revived that; the discussion on the forum shows clearly this is pretty much a requirement at this point. One thought though: shouldn't that extend to classes as well? |
|
Unless a substantial language improvement can be made in terms of moving values around (thereby informing the compiler to elide destructors), precluding invariant in a destructor should be the way to go. T.init must be destructible state, but not necessarily useful state. |
|
I wonder... how hard would it be to achieve an opt-in alternative? E.g. struct S {
invariant() { /* ... */ }
~this() @noinvariant { /* ... */ }
}The wolves would be fed, the sheep would be safe. |
|
I'm all for it in principle, but absent developer (meaning Walter/Andrei) commentary stating otherwise, it seems like the union hack is what we're supposed to use to manage values that may be initialized or not. I don't like it, but I can live with it. |
But it still feels more correct to not destruct anything that's in a I'm still not sure how 19065 is an enhancement and not a bug, as @WalterBright is arguing that a compile-time value should be a valid runtime value? (if i understood correctly). Edit: also wouldn't it just be better code for the destructor not to be called if no type of constructor was called? |
|
It's simple, really. The language is allowed to freely move values on an assumption they're not self-referencing. Ergo, moving values by user code should also be legal. This should work: struct Container(T) {
T data;
void put(T value) {
move(value, data);
}
}And it does so long as there are no invariants involved. But what that does is (potentially) reset struct NotNull(T) {
private T* ptr;
private bool owned;
invariant() { assert(ptr); }
this(T* src, bool own) @safe { ptr = src; owned = own; }
@disable this(this);
~this() @trusted {
if (ptr && own) free(ptr);
}
ref get() inout @safe { return *ptr; }
}This struct is non-copyable, but it can be moved, thereby passing ownership to someone else. Moving it via standard utilities (
We can't enforce that. Especially when dealing with custom allocators. |
|
Thanks for taking the time @radcapricorn :). Ah ok, so the problem is that the compiler cannot know if an object is in its And as for custom allocators, if the compiler knew which objects were in their When a goes out of scope, if it is not in its |
|
Yes, one of the proposed solutions for the issue was to check for the edit: The destructor should probably be skipped in the minimally-initialized case too (memory all zero) so that |
|
I don't think checking the {
A a;
if (someCond())
a = A(wi,th,some,params);
} // is a == A.init? who knows! |
It is, but we're talking about what happens at runtime.
{
A a = customAlloc();
}That code does not even touch the tip of the iceberg as far as custom allocators go.
You can't know that at compile time in D. It's not Rust, it doen't do that kind of flow analysis :) |
|
The nice thing about implicitly checking if the object is in its init state, is if an object in its init state goes out of scope there probably isn't anything useful the destructor can do anyways. |
|
Yeah but that would be a runtime check for every object with a destructor at the end of every scope. |
|
@radcapricorn @thewilsonator ok, I got it now I think. |
This follows up on dlang/dlang.org#2410
Implicit in the implementation of
moveEmplaceis that T.init is always a valid value of T.This isn't necessarily the case, but it isn't actually necessary for it to be the case - all that's necessary is that T.init always be a valid value to call
~this()on. This is because~this()is the only struct method whose invocation is utterly unavoidable.Note that
moveEmplacealready makes this assumption.Maybe a better fix would be checking for
this == typeof(this).init, and running the invariant otherwise - but I don't know how to do that.