-
-
Notifications
You must be signed in to change notification settings - Fork 426
Conversation
Thanks for your pull request, @JinShil! Bugzilla referencesYour PR doesn't reference any Bugzilla issue. If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit uneasy with adding private symbols like isAggregateType
without the underscore to object.d, simply because it pollutes the namespace of every module. I still am not sure this is going to fly when doing recursive destruction.
src/object.d
Outdated
template _isStaticArray(T : U[N], U, size_t N) | ||
private enum bool isPointer(T) = is(T == U*, U) && !isAggregateType!T; | ||
|
||
private enum bool isAggregateType(T) = is(T == struct) || is(T == union) || is(T == class) || is(T == interface); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this is a good idea. private doesn't mean it's not visible, just that you can't use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry, but I don't understand. Are you saying there will be name collisions even if they can't be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you provide an example that illustrates the problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears that this is no longer a problem. It used to be. So objection withdrawn.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It still might confuse, because isAggregateType
is in phobos. If you forget to include the correct module, you may still see a private symbol error. But this isn't as bad as it was before, where it would interfere with public symbols.
destroy(s); | ||
assert(s is null); | ||
assert(S.dtorCount == 1); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I would like to ensure somewhere (doesn't have to be a ddoc unit test), is that the way destroy is implemented, it's not going to recursively follow pointers.
So for instance:
struct S
{
int *x;
}
S s = new S;
int x = 5;
s.x = &x;
destroy(s);
assert(x == 5);
And if I'm reading the code correctly, I think this is going to fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just tested the following using the implementation in this PR:
void main()
{
struct S
{
int *x;
}
S* s = new S;
int x = 5;
s.x = &x;
destroy(s);
assert(x == 5);
}
There is no assertion failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added unit test for this scenario
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, thanks. Yeah, I thought _destructRecurse
was going to destroy recursively, but it correctly just calls the __xdtor
of the struct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, modulo the union
handling.
@@ -2621,7 +2621,7 @@ unittest | |||
} | |||
|
|||
private void _destructRecurse(S)(ref S s) | |||
if (is(S == struct)) | |||
if (is(S == struct) || is(S == union)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this can work for union
s whose members have conflicting values of T.init
, or any union members with non-trivial ctor/dtors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The union support was added after Jenkins failed due to this code:
@bbasile Do you have an thoughts on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears destructors are not allowed for union
s:
union U
{
bool b;
uint u;
~this() { } // Error: destructor `onlineapp.U.~this` destructors, postblits and
// invariants are not allowed in union U
}
So, we should probably disallow destroy
for union
s, emitting a compile-time error if it is attempted. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Users should manually specify the union member they want to destroy.
} | ||
|
||
// Ensures pointers are not recursively followed | ||
nothrow @system unittest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think adding pure
here makes sense, given that destroying struct
s with trivial destructors is completely deterministic and won't cause access to any shared mutable state.
src/object.d
Outdated
|
||
private enum bool isAggregateType(T) = is(T == struct) || is(T == union) || is(T == class) || is(T == interface); | ||
|
||
private template isStaticArray(T : U[N], U, size_t N) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace with:
private enum isStaticArray(T) = is(T : U[N], U, size_t N);
|
||
unittest | ||
{ | ||
static union uni { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, I believe this is would be misfeature, as only user code can know how to correctly destroy a union, usually by means of a discriminating field in the containing aggregate. I think the behavior implemented here is "the last union member wins", i.e. the last union member will determine the value of the whole union.
Also, here's an example which should never compile:
struct Str
{
char* s;
~this() { free(s); }
}
struct Num
{
size_t value = 0xDEAD_BEEF;
}
union U
{
Num number;
Str string;
}
U u;
u.number.value = 42;
destroy(u);
In all likelihood the code above will end up attempting to free
the memory at address 0xDEAD_BEEF
, after setting u.number.value
to this value.
Not sure why, but it appears freebsd32 keeps timing out on some test. |
See this: braddr/d-tester#70 |
I think we should go a different way with destroy. Starting from the documented behavior in https://dlang.org/phobos/object.html#.destroy: "Destroys the given object and puts it in an invalid state. It's used to destroy an object so that any cleanup which its destructor or finalizer does is done and so that it no longer references any other objects. It does not initiate a GC cycle or free any GC memory." First comment: "puts it in an invalid state" is a very, very lax terminology that provides no guarantee of the value of the object thereafter. So an observation is that for non-aggregate types we can simply delete the line https://github.com/dlang/druntime/blob/master/src/object.d#L3150. Even I think a better wording is: "Destroys the given object. It's used to destroy an object so that any cleanup which its destructor or finalizer does is done and so that it no longer references any other objects. It does not initiate a GC cycle or free any GC memory. If the object has a destructor then it is default-initialized from Second matter: Some effort (which this PR is taking a bit further) has been invested in making (Related to this PR in particular: the fact that void fun(S * p) {
destroy(p);
p.x = 42; // oops
} The code above is correct with |
To create an action item out of these, I think we should:
The milder version that does not break its documentation:
I think we should do the mild version now and discuss deprecation after that. |
@andralex, I agree with pretty much all you said, having destroy that "does nothing" is a placebo that says "You did it right, because it compiled!" Much better to error so the user cannot accidentally use it. One note:
What we still are missing (even with this PR) is something that destroys all the elements of a dynamic array. As I said elsewhere, Would it make sense to modify |
Invalid State
That documentation is already out of date. 2.079 (the upcoming release) already has an update to that documentation (as a result of #2054) that reads: "Destroys the given object and sets it back to its initial state. It's used to destroy an object, calling its destructor or finalizer so it no longer references any other objects. It does not initiate a GC cycle or free any GC memory. " So the verbage about "invalid state" has already been removed, though it still may not be correct, depending on the semantics we decide on here. Re-initializing only aggregate types
Please forgive my ignorance, but I'm not sure I understand the rationale. I'd prefer uniform behavior, if possible. That is, if reference types are re-initialized in So, unless I'm missing something, I'd prefer it if all types were re-initialized in
|
Error please.
Error please.
Re-initialization for structs without destructors. It is what is expected I would think. Yes, it's more expensive than no-op, but uniformity is more important here.
If destroy isn't going to actually destroy the elements of the array, I would say it should Error (similar to the pointer case). Otherwise, I would support destroying the elements. Currently, it just sets the array to null. You may ask why I would want different behavior from arrays and pointers? It's because dynamic arrays are considered to be the elements themselves, not just pointer and length. We use that to great effect everywhere. Alternatively, you could make destroy on an array an Error, and introduce a new function that will do the destruction of the elements. |
What about removing re-initialization for all types? Is that an option? |
I think this needs a slight modification as it sounds like a guarantee for nogc, but called destructors are still able to produce arbitrary GC activity. |
It is a possible option, but I think that is more disruptive. People expect struct reinitialization, their code probably counts on it.
Yes, but |
So I've been working on this for a few hours and I now realize I can't do all of these:
If we decide to re-initialize for all types, then I have to implement So I'm faced with the following two implementation choices: Implementation 1 -
Implementation 2 -
Either 1 or 2, or we break uniformity. |
Uniformity is not what we're looking for here. We look for simple, composable, efficient primitives. It's much easier to detect "does destroy apply to type T" than "would calling destroy on type T result in a no-op?" So we want to implement destroy for the cases in which it does real work. For types with destructor, it's important that destroy leaves them in a destructible state, i.e. the type is re-inited. Otherwise any failed attempt to reinitialize them (or just doing nothing) will leave them in an invalid state when the object goes out of scope. So that's important. For all other types, it's For static and dynamic arrays that have elements with destructors, Now it comes down to code breakage. If we deprecate all calls that do nothing, well that's some breakage. If we leave them as they are, people can just call |
I reckon I'm not being the most coherent because I'm unclear on when I talk "what I think we could do if we did destroy from scratch" and when I talk about "what I think we can do with destroy right now". Briefly, destroy must ensure the object has no additional resources allocated and is in a state in which it can safely be destroyed (e.g. go out of scope, or be destroyed again during collection). This is the only uniformity we're after - we're not looking for leaving the object in a specific state, only one that is not dangerous. So for an |
So, if I understand correctly, |
Destructors aren't generally designed to be called multiple times on the same object. For instance, what if a struct's destructor calls a member function on a class reference and then calls |
A very simple situash would be: struct S { void* p; ... ~this() { ... free(p); ... } } Calling the destructor twice in a row is liable to double-free memory.
Not sure I understand the question - two negations make it difficult. Let me try to rephrase whilst eliminating both negations: "How would re-initializing instances of types with destructors put the instance in a benign (destructible) state?" If that's the question: Types need to handle the situation whereby the destructor is called against |
So part of the contract of
What about allowing |
No, just that destroy leaves the object in a destructible state. If the object has no destructor, any state is destructible (including states that would cause problems with multiple calls to destroy).
To the extent possible it's best to move from more restrictive to less - the other way around breaks code. So I'd just disable that use for the time being. |
I need a better defininition of "destructible state". Is it "not referring to any resources not managed by the GC"? |
@JinShil bummer about the long exchange - this would be hashed out in 10 minutes in a room with a whiteboard. Consider: class C { ... }
void fun() {
auto c = new C;
destroy(c);
c = null;
GC.collect;
} This code should be safe. In particular the call to C's destructor during collection should be legit. Also: struct S { ... }
void fun() {
S s = ...;
destroy(s);
} Again the code should be safe. Note that the dtor is called when s goes out of scope. So... "destructible state" means "the object should be in a state such that its destructor can be called with well-defined behavior". |
This PR adds an overload for
destroy
that automatically dereferences pointers in an attempt to make the usage of destroy more intuitive (at least to some).This is an alternative to #2112. See discussion there, and the discussion starting at dlang/dmd#7947 (comment) for context.
Also see http://ddili.org/ders/d.en/memory.html#ix_memory.destroy
Caveat:
To keep the contract of
destroy
it also destroys the pointer itself, reinitializing it back tonull
. The user will, therefore, no longer have a valid pointer to callGC.free
with. If the user does want to callGC.free
then they will have to usedestroy(*ptr);
themselves to destroy on the object referenced by the pointer and not the pointer itself.If this is merged, #2112 is not needed.
cc @schveiguy @wilzbach