www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - How to cast `shared` away reliably

reply Arafel <er.krali gmail.com> writes:
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
parent reply "H. S. Teoh" <hsteoh qfbox.info> writes:
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
next sibling parent reply Arafel <er.krali gmail.com> writes:
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:
 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
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.
Feb 10
parent reply Nick Treleaven <nick geany.org> writes:
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 why
You need to declare `alias n` or `size_t n` to match a static array, `n` is not a type.
Feb 11
parent Arafel <er.krali gmail.com> writes:
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
prev sibling parent reply Nick Treleaven <nick geany.org> writes:
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
parent reply Arafel <er.krali gmail.com> writes:
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
parent reply Paul Backus <snarwin gmail.com> writes:
On Wednesday, 11 February 2026 at 12:38:37 UTC, Arafel wrote:
 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?
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:
 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
parent reply Arafel <er.krali gmail.com> writes:
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:
 
 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
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.
Feb 11
next sibling parent reply Nick Treleaven <nick geany.org> writes:
On Wednesday, 11 February 2026 at 16:49:19 UTC, Arafel wrote:
 On 2/11/26 16:02, Paul Backus wrote:
 Source: https://dlang.org/spec/const3.html
Quite helpful even if still somewhat confusing. I think it would still make sense to define what a "subtype" is.
Good idea.
 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; // FAILS
Same 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; // FAILS
Because 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
parent reply Arafel <er.krali gmail.com> writes:
On 2/11/26 22:12, Nick Treleaven wrote:
 On Wednesday, 11 February 2026 at 16:49:19 UTC, Arafel wrote:
 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 ```
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.
 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.
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[])`.
Feb 11
parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
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
prev sibling parent Paul Backus <snarwin gmail.com> writes:
On Wednesday, 11 February 2026 at 16:49:19 UTC, Arafel wrote:
 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:
 
 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
Quite helpful even if still somewhat confusing. I think it would still make sense to define what a "subtype" is.
The usage of the word "subtype" here is incorrect; it should probably say something like "component types" instead.
 However, 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