digitalmars.D - How to cast `shared` away reliably
- Arafel (62/62) Feb 10 So, first of all, a bit of background - I just hit this regression:
- H. S. Teoh (8/13) Feb 10 [...]
- Arafel (47/63) Feb 10 Nope, exact same issue:
- Nick Treleaven (6/16) Feb 11 Due to pattern matching, it seems correct. But see also
- Arafel (4/7) Feb 11 Ops, my bad! In my defense, I'll say that I had been dealing with this
- Nick Treleaven (4/9) Feb 11 There is also `Unshared`:
- Arafel (7/13) Feb 11 Interesting, it seems the implementation is the second one I tried:
- Paul Backus (9/18) Feb 11 All type qualifiers in D (`const`, `immutable`, `shared`) are
- Arafel (39/52) Feb 11 Quite helpful even if still somewhat confusing.
- Nick Treleaven (30/59) Feb 11 A type qualifier on a delegate type applies to its context
- Arafel (41/63) Feb 11 But:
- Richard (Rikki) Andrew Cattermole (2/15) Feb 11 https://forum.dlang.org/post/pwhgwshfxfunwtflhmfz@forum.dlang.org
- Paul Backus (14/34) Feb 11 The usage of the word "subtype" here is incorrect; it should
So, first of all, a bit of background - I just hit this regression: https://github.com/dlang/dmd/issues/22556 TLDR: I just want / need a reliable way to get exactly `T` from `shared (T)`. The core question is that there is no reliable way to "peel" away `shared`, at leas from slices or AAs. You can do it if you cast to the exact (unshared) type by hand, but this isn't always feasible, especially in generic / templated code. IOW, one would reasonably expect that for any type `T`, and `S` being `shared (T)`, if you use `cast ()` on a variable of type `S`, you'd get back `T`, right? ```d alias Unshared(T) = typeof(cast() T.init); template sanity(T) { alias S = shared(T); static if(is(T==Unshared!S)) { enum sanity = true; } else { enum sanity = false; } } static assert(sanity!int); // PASSES static assert(sanity!(int[])); // FAILS static assert(sanity!(int[int])); // FAILS ``` OK, let's try a more powerful `is` expression then: ```d template BetterUnshared(S) { static if (is(S == shared(T), T)) { alias BetterUnshared = T; } } template sanity(T) { alias S = shared(T); static if(is(T == BetterUnshared!S)) { enum sanity = true; } else { enum sanity = false; } } static assert(sanity!int); // still PASSES static assert(sanity!(int[])); // still FAILS static assert(sanity!(int[int])); // still FAILS ``` And we still get an extra `shared` popping out of nowhere: ```d alias SII = shared(int[int]); template BetterUnshared(S) { static if (is(S == shared(T), T)) { alias BetterUnshared = T; } } pragma(msg, BetterUnshared!SII); // shared(int)[int] ``` I assume the root cause is this: ```d static assert(is(shared(int[]) == shared(shared(int)[]))); static assert(is(shared(int[int]) == shared(shared(int)[int]))); ``` Is this desirable / documented anywhere? Again, I just want / need a reliable way to get exactly `T` from `shared (T)`.
Feb 10
On Tue, Feb 10, 2026 at 10:14:36PM +0100, Arafel via Digitalmars-d wrote:So, first of all, a bit of background - I just hit this regression: https://github.com/dlang/dmd/issues/22556 TLDR: I just want / need a reliable way to get exactly `T` from `shared (T)`.[...] I thought Phobos has an Unqual!T template that does exactly that. Well OK, it strips away more than just shared, but you should be able to achieve what you want by looking at how Unqual is implemented. T -- In a world without fences, who needs Windows and Gates? -- Christian Surchi
Feb 10
On 2/10/26 23:56, H. S. Teoh wrote:On Tue, Feb 10, 2026 at 10:14:36PM +0100, Arafel via Digitalmars-d wrote:Nope, exact same issue: ```d import std; alias SII = shared(int[int]); pragma(msg, Unqual!SII); // still "shared(int)[int]" ``` And, for the record, I _can_ create a workaround that passes all the tests that I could come up with: ```d template ReallyUnshared(S) { static if (is(S == shared(K[V]), K,V)) { alias ReallyUnshared = K[V]; // Why is `K` not shared here? Who knows... } else static if (is(S == shared(T[]), T)) { // Same here alias ReallyUnshared = T[]; // } else static if (is(S == shared(T[n]), T,n)) { // You'd expect this to work, but it doesn't // alias ReallyUnshared = T[n]; // Don't ask me why } else static if (__traits(isStaticArray, S)) { alias ReallyUnshared = typeof(cast () S.init[0])[S.length]; } else static if (is(S == shared(T), T)) { alias ReallyUnshared = T; } else { alias ReallyUnshared = S; } } template sanity(T) { alias S = shared(T); static if(is(T == ReallyUnshared!S)) { enum sanity = true; } else { enum sanity = false; } } static assert(sanity!int); static assert(sanity!(int[])); static assert(sanity!(int[5])); static assert(sanity!(int[int])); ``` The problem is: a) It's a kludge, ugly as hell. b) I have no way of knowing what other corner cases I might be missing. c) There *MUST* be a clean way to do it. d) It's terribly inconsistent behaviour and, I suspect, unintended: I would be relying on something that could change with some random refactoring or whatever.So, first of all, a bit of background - I just hit this regression: https://github.com/dlang/dmd/issues/22556 TLDR: I just want / need a reliable way to get exactly `T` from `shared (T)`.[...] I thought Phobos has an Unqual!T template that does exactly that. Well OK, it strips away more than just shared, but you should be able to achieve what you want by looking at how Unqual is implemented. T
Feb 10
On Wednesday, 11 February 2026 at 02:02:20 UTC, Arafel wrote:```d template ReallyUnshared(S) { static if (is(S == shared(K[V]), K,V)) { alias ReallyUnshared = K[V]; // Why is `K` not shared here? Who knows...Due to pattern matching, it seems correct. But see also https://github.com/dlang/dmd/issues/22182. I did work on a fix for that but haven't got it 100% yet.} else static if (is(S == shared(T[]), T)) { // Same here alias ReallyUnshared = T[]; // } else static if (is(S == shared(T[n]), T,n)) { // You'd expect this to work, but it doesn't // alias ReallyUnshared = T[n]; // Don't ask me whyYou need to declare `alias n` or `size_t n` to match a static array, `n` is not a type.
Feb 11
On 2/11/26 12:44, Nick Treleaven wrote:You need to declare `alias n` or `size_t n` to match a static array, `n` is not a type.Ops, my bad! In my defense, I'll say that I had been dealing with this confusing issue for too long, so when I found it didn't work as I expected, I wasn't in the mood to think about it anymore.
Feb 11
On Tuesday, 10 February 2026 at 22:56:43 UTC, H. S. Teoh wrote:I thought Phobos has an Unqual!T template that does exactly that. Well OK, it strips away more than just shared, but you should be able to achieve what you want by looking at how Unqual is implemented.There is also `Unshared`: https://dlang.org/phobos/std_traits.html#Unshared It only strips head shared though.
Feb 11
On 2/11/26 12:26, Nick Treleaven wrote:There is also `Unshared`: https://dlang.org/phobos/std_traits.html#Unshared It only strips head shared though.Interesting, it seems the implementation is the second one I tried: https://github.com/dlang/phobos/blob/v2.112.0/std/traits.d#L7898-L7904 So it seems this is known and documented, even with a test / example:Only explict shared is removed.static assert(is(Unshared!(shared(int[])) == shared(int)[]));How can I know what _implicit_ shared there are? Is there a rationale for them, or somewhere in the spec where they are mentioned? Or it is how it is, and that's all there is to it?
Feb 11
On Wednesday, 11 February 2026 at 12:38:37 UTC, Arafel wrote:All type qualifiers in D (`const`, `immutable`, `shared`) are transitive, which means that a type qualifier applied to the "top level" of a type will also be applied recursively to each part of the type. So, a `const(int[])` is the same as a `const(const(int)[])`, and likewise for `shared`, `immutable`, or any combination thereof. This is documented in the language spec's page on type qualifiers:Only explict shared is removed.static assert(is(Unshared!(shared(int[])) == shared(int)[]));How can I know what _implicit_ shared there are? Is there a rationale for them, or somewhere in the spec where they are mentioned? Or it is how it is, and that's all there is to it?Type qualifiers modify a type by applying a _TypeCtor_. _TypeCtors_ are: `const`, `immutable`, `shared`, and `inout`. Each applies transitively to all subtypes.Source: https://dlang.org/spec/const3.html
Feb 11
On 2/11/26 16:02, Paul Backus wrote:All type qualifiers in D (`const`, `immutable`, `shared`) are transitive, which means that a type qualifier applied to the "top level" of a type will also be applied recursively to each part of the type. So, a `const(int[])` is the same as a `const(const(int)[])`, and likewise for `shared`, `immutable`, or any combination thereof. This is documented in the language spec's page on type qualifiers:Quite helpful even if still somewhat confusing. I think it would still make sense to define what a "subtype" is. It might feel "intuitive", but the closest I can find is this: https://dlang.org/spec/type.html#derived-data-types However, of the items listed there, pointers are indeed showing this behaviour, but functions and delegates aren't. Also, it wasn't clear to me what classes and structs would do it (they don't, either). So, all in all, this becomes one of the most unintuitive parts of the language. Consider: ```d struct Pointer(T) { T* t; alias this=t; } void main() { shared(Pointer!int) foo; shared(int *) bar; int i; foo = &i; // FAILS bar = &i; // FAILS cast() foo = &i; // WORKS!! cast() bar = &i; // FAILS } ``` So, I'm not saying that it's not according to spec, or that it's wrong, but it really _looks like_ the unintended result of applying rules meant for other situations rather than a fully thought-out feature. I can _now_ understand what is happening, and more or less why. But when you have a shared variable and just want to remove `shared` after having made sure you've got whatever locks you need, you need to dig really deep into the obscure corner cases of the language, especially if you're using it in generic code. So my suggestion would be to have some `unshare` helper in phobos, with lowercase "u", and potentially `unconst`, etc. These functions would get you back your variable cast back as `T` when you pass in a `shared(T)` one. Assuming, of course, that the current behaviour isn't going to change anytime soon.Type qualifiers modify a type by applying a _TypeCtor_. _TypeCtors_ are: `const`, `immutable`, `shared`, and `inout`. Each applies transitively to all subtypes.Source: https://dlang.org/spec/const3.html
Feb 11
On Wednesday, 11 February 2026 at 16:49:19 UTC, Arafel wrote:On 2/11/26 16:02, Paul Backus wrote:Good idea.Source: https://dlang.org/spec/const3.htmlQuite helpful even if still somewhat confusing. I think it would still make sense to define what a "subtype" is.It might feel "intuitive", but the closest I can find is this: https://dlang.org/spec/type.html#derived-data-types However, of the items listed there, pointers are indeed showing this behaviour, but functions and delegates aren't.A type qualifier on a delegate type applies to its context pointer, it does nothing for function (pointer) types.Also, it wasn't clear to me what classes and structs would do it (they don't, either).They do: ```d struct S { int i; } shared S s; pragma(msg, typeof(s.i)); // shared int ```So, all in all, this becomes one of the most unintuitive parts of the language. Consider: ```d struct Pointer(T) { T* t; alias this=t; } void main() { shared(Pointer!int) foo; shared(int *) bar; int i; foo = &i; // FAILS`foo.t` is `shared(int*)`. You cannot assign `int*` to that. Note that these work: ```d foo = bar; foo.t = bar; ```bar = &i; // FAILSSame as `foo.t = &i`.cast() foo = &i; // WORKS!!I think due to the `alias this` it is actually doing this: (cast() foo).t = &i; // WORKS!! That is a bit confusing. The cast gives you `Pointer!int`. I think the docs here need to mention that: https://dlang.org/spec/expression.html#cast_qualifier Note that this does not work: cast() foo.t = &i; // error, `shared(int)*` = `int*`cast() bar = &i; // FAILSBecause that does `shared(int)*` = `int*`, which is a type error.So, I'm not saying that it's not according to spec, or that it's wrong, but it really _looks like_ the unintended result of applying rules meant for other situations rather than a fully thought-out feature.I don't think the type qualifiers are at fault, its more how casting and `alias this` interact with them.
Feb 11
On 2/11/26 22:12, Nick Treleaven wrote:On Wednesday, 11 February 2026 at 16:49:19 UTC, Arafel wrote:But: ```d struct S { int i; int* p; } shared S s; static assert(is(typeof( (cast() s).i) == int)); static assert(is(typeof( (cast() s).p) == int*)); ``` So with a struct or class you can cast away `shared` to access their members, but with a built-in array or AA you can't. As an example of why this can become confusing, consider the different behaviour that `Array` (or any container type that has opIndex) has: ```d import std; alias AI = Array!int; shared AI ai; static assert(is(typeof((cast () ai)[0]) == int)); // PASSES alias AI2 = int[]; shared AI2 ai2; static assert(is(typeof((cast () ai2)[0]) == int)); // FAILS ``` My purpose here, as in the previous example, was to show how user-defined types and built-in types behave differently, even when they look similar, and how that can easily become confusing.On 2/11/26 16:02, Paul Backus wrote: Also, it wasn't clear to me what classes and structs would do it (they don't, either).They do: ```d struct S { int i; } shared S s; pragma(msg, typeof(s.i)); // shared int ```After spending so much time, I more or less understand now how it works, and why some tests pass, and some don't. I just think it's too inconsistent, confusing, and ultimately unnecessary. These rules make sense (perhaps) for `const`, but certainly not for `shared` that it _expected_ to be removed. If the language _forces_ me to cast away shared (and, for the record, I agree with that), it should be easy and straightforward. There should be a clear way to say: "Hey, all locks in place, now it's safe. Give me my variable back exactly as it was, so I can actually do some work." This just forces one to go through (even more) hoops and loops to be able to use a `shared` variable, especially in generic code. And, in fact, it's even messier if you add complex types, like `shared (RefCounted!S[string])` and / or `shared(RefCounted!S[])`.So, I'm not saying that it's not according to spec, or that it's wrong, but it really _looks like_ the unintended result of applying rules meant for other situations rather than a fully thought-out feature.I don't think the type qualifiers are at fault, its more how casting and `alias this` interact with them.
Feb 11
On 12/02/2026 2:14 PM, Arafel wrote:These rules make sense (perhaps) for |const|, but certainly not for | shared| that it /expected/ to be removed. If the language /forces/ me to cast away shared (and, for the record, I agree with that), it should be easy and straightforward. There should be a clear way to say: "Hey, all locks in place, now it's safe. Give me my variable back exactly as it was, so I can actually do some work." This just forces one to go through (even more) hoops and loops to be able to use a |shared| variable, especially in generic code. And, in fact, it's even messier if you add complex types, like |shared (RefCounted!S[string])| and / or |shared(RefCounted!S[])|.https://forum.dlang.org/post/pwhgwshfxfunwtflhmfz forum.dlang.org
Feb 11
On Wednesday, 11 February 2026 at 16:49:19 UTC, Arafel wrote:On 2/11/26 16:02, Paul Backus wrote:The usage of the word "subtype" here is incorrect; it should probably say something like "component types" instead.All type qualifiers in D (`const`, `immutable`, `shared`) are transitive, which means that a type qualifier applied to the "top level" of a type will also be applied recursively to each part of the type. So, a `const(int[])` is the same as a `const(const(int)[])`, and likewise for `shared`, `immutable`, or any combination thereof. This is documented in the language spec's page on type qualifiers:Quite helpful even if still somewhat confusing. I think it would still make sense to define what a "subtype" is.Type qualifiers modify a type by applying a _TypeCtor_. _TypeCtors_ are: `const`, `immutable`, `shared`, and `inout`. Each applies transitively to all subtypes.Source: https://dlang.org/spec/const3.htmlHowever, of the items listed there, pointers are indeed showing this behaviour, but functions and delegates aren't.What you are referring to as "function types" are probably function _pointer_ types. Qualifiers work the same way for them as they do for any other pointer types. Note that the _parameters_ of a function are not affected by qualifiers on a function pointer, because the parameters of a function are neither _contained in_ nor _pointed to_ by the function pointer. For delegates, the qualifier _should_ apply to both the function pointer and the context pointer, but iirc there are some outstanding bugs in the compiler related to this, so it may not work correctly in all cases.
Feb 11









Arafel <er.krali gmail.com> 