Created attachment 1119
repro case
If a struct a has another struct b as a member which has a destructor and the
struct a is created as shared, the destructor of b can not be called.
The error message dmd generates is:
Error: destructor main.bar.~this () is not callable using argument types ()
Tested with dmd 2.059
This prevents one from using shared properly together with structs.
See attached repro case
Comment #1 by bugzilla — 2014-11-13T11:01:29Z
Here's the repro case from the attachment:
struct foo
{
char[] buf;
this(size_t size)
{
buf = new char[size];
}
~this()
{
buf = null;
}
}
struct bar
{
foo f;
}
shared bar b;
Comment #2 by r9shackleford — 2016-02-12T17:32:10Z
*** Issue 12004 has been marked as a duplicate of this issue. ***
Comment #3 by r9shackleford — 2016-02-12T17:32:44Z
*** Issue 13174 has been marked as a duplicate of this issue. ***
Comment #4 by r9shackleford — 2016-02-12T17:37:18Z
dmd 2.070
Error: non-shared method f642.bar.~this is not callable using a shared object
marked two issues as duplicates that are caused by the same problem/require the same fix(AFAICT)
on topic:
a shared destructor doesn't really make sense at all, shouldn't shared objects just have unshared destructors?
Comment #5 by z.p.gaal.devel — 2017-06-19T22:03:08Z
the same for this code too:
struct foo
{
int a;
}
struct bar
{
foo f;
~this()
{
}
}
shared bar b;
Comment #6 by atila.neves — 2018-01-27T12:19:29Z
*** Issue 6804 has been marked as a duplicate of this issue. ***
Comment #7 by dfj1esp02 — 2018-02-12T16:09:44Z
(In reply to weaselcat from comment #4)
> on topic:
> a shared destructor doesn't really make sense at all, shouldn't shared
> objects just have unshared destructors?
It means implicit cast from shared to unshared, which required uniqueness, which can't be guaranteed by compiler, so such implicit cast is unsafe. Another classic example is reference counted data that needs to know whether it's shared or not and use atomic arithmetic when needed.
Examples above should be rejected at compile time, because they assume the unshared destructor is safe to call on shared data, which is against the design of shared: sharing must be opt-in.
Comment #8 by Marco.Leise — 2018-02-12T20:25:22Z
The implication is that when a thread destroys an object it already uniquely owns it.
Comment #9 by dfj1esp02 — 2018-02-13T08:05:05Z
It might own the struct, but not the referenced data. Again example is reference counter: destruction can depend on sharing. Another example is that shared data can't be (de)allocated with thread local allocator.
Comment #10 by Marco.Leise — 2018-02-17T11:08:21Z
Thanks for the clarification. Shared has never been fully fleshed out. What we can currently take away from the specification is this:
"8.8 shared Attribute
The shared attribute modifies the type from T to shared(T), the same way as
const does."
My thinking is that top level qualifiers on assignments and function parameters only have informative character. Sometimes we want to enforce a coding practice that says that function parameters are no scratch space or make it easier to reason about a piece of code by ensuring that variables cannot be modified after the initial assignment. In any case these are copies of the values that are assigned to them.
You can tell where I'm going: Copy a shared reference counting struct and it is no longer shared except for the data it references. So in my brain there is no "shared(RefCounter!T)" that atomically decrements the counter. For that I would have to create another "SharedRefCounter". Whether or not the payload is shared is independent of that.
I don't know what I can say about thread local allocators. You can still share data allocated with them as long as upon destruction you queue the item in for freeing on the correct thread's "items to be free'd list". It all has to be manually implemented without help from the language.
Comment #11 by Marco.Leise — 2018-02-17T11:38:39Z
Uh, I think I just contradicted my two years younger self from the linked bug report. I guess whether or not `stdout` (or any reference counter) should use atomic operations must not depend on whether the struct itself is `shared` for the afore mentioned reasons. `stout` being a global variable should of course be `shared`, but threads are free to create a local, unshared copy of that RC pointer. These copies would still uses atomic operations internally. That way I can reconcile the both of me.
TL;DR Let's not use shared to signify whether an RC pointer should use atomic ops internally. Value types can always be copied stripping their top-level qualifiers!
Comment #12 by dfj1esp02 — 2018-02-19T08:49:11Z
(In reply to Marco Leise from comment #10)
> You can tell where I'm going: Copy a shared reference counting struct and it
> is no longer shared except for the data it references.
If you can solve it for const, the same solution can work for shared, but it doesn't deny overloading on qualifiers.
> So in my brain there
> is no "shared(RefCounter!T)" that atomically decrements the counter. For
> that I would have to create another "SharedRefCounter". Whether or not the
> payload is shared is independent of that.
It can be done either way, albeit the latter can provide more safety. In the end it's a design decision.
> I don't know what I can say about thread local allocators. You can still
> share data allocated with them as long as upon destruction you queue the
> item in for freeing on the correct thread's "items to be free'd list".
The destructor needs to know whether it needs to resort to this or not. Shared destructor would do it, unshared wouldn't.
> Value types can always be copied stripping their top-level qualifiers!
That's fine, but this problem isn't solved yet for transitive qualifiers.
Comment #13 by Marco.Leise — 2018-02-20T20:04:26Z
(In reply to anonymous4 from comment #12)
> (In reply to Marco Leise from comment #10)
> > You can tell where I'm going: Copy a shared reference counting struct and it
> > is no longer shared except for the data it references.
> If you can solve it for const, the same solution can work for shared, but it
> doesn't deny overloading on qualifiers.
> [...]
> > Value types can always be copied stripping their top-level qualifiers!
> That's fine, but this problem isn't solved yet for transitive qualifiers.
No it is not fine, because if we agree that top level qualifiers are free to change after a copy, and it seems sensible to say that the shared status of a copy is opt-in, then how would Dlang know that the struct copy should be destructed with the shared (or const to keep the analogy) destructor?
Against the spec that put shared and const on equal ground, you would have to force all copies to be shared, too. Personally I feel the spec is right: The moment you copy something, it is thread-local and mutable. It may contain pointers to data that is const or shared, but that is on a different level. That's why I said there can only be a SharedRefCounter and that shared(RefCounter) doesn't work.
It is for example common to enter a critical section that protects some shared variables and then cast away shared. You could then blit a previously shared reference counting pointer variable to a local copy and work with that. Upon destruction it wouldn't know that it should apply shared semantics to the reference count.
The problem we have understanding each other comes from shared not being fleshed out. For me it is a tag that you apply temporarily while data is _currently_ in use by multiple threads, just so you don't casually read from or write to it and expect it to work. For you it has semantic meaning as a type constructor, sticks with copies and eventually decides which destructor overload to use. And since I was in the same camp two years ago I may change my mind again, but ultimately a decision has to be made here.
Comment #14 by dfj1esp02 — 2018-02-22T20:51:26Z
(In reply to Marco Leise from comment #13)
> No it is not fine, because if we agree that top level qualifiers are free to
> change after a copy, and it seems sensible to say that the shared status of
> a copy is opt-in, then how would Dlang know that the struct copy should be
> destructed with the shared (or const to keep the analogy) destructor?
> Against the spec that put shared and const on equal ground, you would have
> to force all copies to be shared, too.
Implicit conversion for structs without indirection took precedence here. In practice such struct seldom need elaborate management and you can opt out. Though the same argument is true for const too.
> It is for example common to enter a critical section that protects some
> shared variables and then cast away shared.
As a general note you should consider what you do when you implement manual multithreading. It can be also designed in many ways, casting may be not part of the design.
> The problem we have understanding each other comes from shared not being
> fleshed out.
Your complaints are applicable to const too. That would mean const is not fleshed out either?
> For me it is a tag that you apply temporarily while data is
> _currently_ in use by multiple threads, just so you don't casually read from
> or write to it and expect it to work.
Implicit casting violates this reasoning. When data used by multiple threads is implicitly casted to thread local, how can it work in your understanding? What will disallow to casually use it? Like for const. If you want shared data to be different, implicit casting erases that difference without a trace, and the point is lost, it becomes C type system where anything converts to anything silently.
Comment #15 by moonlightsentinel — 2021-11-01T14:55:07Z