How to cast `shared` away reliably
Arafel
er.krali at gmail.com
Thu Feb 12 17:31:03 UTC 2026
On 2/12/26 15:59, Kagamin wrote:
> Example:
> ```d
> shared class Unshareable(T)
> {
> T value;
> T castToUnshared()
> {
> synchronized(this)
> {
> return cast(T)value;
> }
> }
> void castFromUnshared(T value2)
> {
> synchronized(this)
> {
> value=cast(shared)value2;
> }
> }
> }
>
> shared Unshareable!(int[][]) a;
>
> unittest
> {
> int[][] b=a.castToUnshared();
> a.castFromUnshared(b);
> shared int[][] b2=a.value; //shared
> }
> ```
I understand that this is just a proof of concept, but this has quite a
few of issues.
For starters, if you use value types, you are returning a copy each time
you switch from shared to unshared and vice versa:
```d
shared class Unshareable(T)
{
T value;
T castToUnshared()
{
synchronized(this)
{
return cast(T)value;
}
}
void castFromUnshared(T value2)
{
synchronized(this)
{
value=cast(shared)value2;
}
}
}
struct S {
@disable this(this);
}
shared Unshareable!(S) a; // FAILS both in `castToUnshared` and
`castFromUnshared`
```
And then you'll find out that `cast (T) value` happens to be an rvalue,
so you can't just return `ref`, unlike `cast () value`... that is an
lvalue. Welcome to the rabbit hole.
Of course you can start adding corner cases, but you end up with a mess
making you wonder whether you should just forget about it and just go
with `__gshared` all the way.
Here is my current approach for comparison, and the kind of solution I
think should be, at least, in phobos:
```d
template Unshared(S) {
static if (is(S == shared(K[V]), K,V)) {
alias Unshared = Unshared!(K)[V];
} else static if (is(S == shared(T[]), T)) {
alias Unshared = Unshared!(T)[];
} else static if (is(S == shared(T[n]), T,size_t n)) {
alias Unshared = Unshared!(T)[n];
} else static if (is (S == shared(T*), T)) {
alias Unshared = Unshared!(T)*;
} else static if (is(S == shared(T), T)) {
alias Unshared = T;
} else {
alias Unshared = S;
}
}
unittest {
template sanity(T) {
alias S = shared(T);
static if(is(T == Unshared!S)) {
enum sanity = true;
} else {
enum sanity = false;
}
}
static assert(sanity!int);
static assert(sanity!(int[]));
static assert(sanity!(int[][]));
static assert(sanity!(int[int][5][]*));
static assert(sanity!(int[5]));
static assert(sanity!(int[int]));
static assert(sanity!(int*));
}
pragma(inline, true)
ref unshare(T)(return ref T t) => *(cast (Unshared!(T*)) &t);
import std;
struct S {
@disable this(this);
int*[] i;
int *j;
RefCounted!int k;
}
unittest {
// Our original data
S s;
s.j = new int;
s.k = 1;
// Make it ready to be shared
ref ss = cast (shared) s;
// Send ss to other thread(s), then eventually afer being sure
we're in critical section, get our data back
ref us = ss.unshare;
// Now we can do business as usual, forgetting that `shared` was
ever involved
int j = 42;
us.i ~= &j;
*(us.j) = 13;
us.k = 2;
// No need to switch back to shared, we changed the original value
directly
assert(*(s.i[0]) == 42);
assert(*(s.j) == 13);
assert(s.k == 2);
}
```
I find it much cleaner and more intuitive, tbh.
More information about the Digitalmars-d
mailing list