csharplang
csharplang copied to clipboard
Consider changing the inferred type of stackalloc to a Span<T> instead of a T* if using a safe context
Currently, if you want to use stackalloc to allocate a Span<T> you need to do one of the following because the inferred type when using the var keyword is a T* (which you cannot access in a safe context.
Span<T> span = stackalloc T[size]
or
var span = (Span<T>) stackalloc T[size]
It would be nice if this so that you could do something like the following and get a Span<T> as the inferred type if you were not using an unsafe context
var span = stackalloc T[size]
This is a language proposal, not a framework/runtime proposal. Language changes should be proposed at dotnet/csharplang.
But on top of that, I don't see this being considered because it changes meaning of the code depending on whether the current block is unsafe or not.
As @Joe4evr said, we likely can't do this. If we had Span when stackalloc was added to the language, it probably would never have returned a pointer array. But it did, and having the result type of an expression change based on the unsafe context does not sound like a good idea to me.
I'm personally intrigued by this idea. Namely, while it would technically be a breaking change , I wonder if we could justsay that the type of stackalloc was Span<T>? We could also say there was an implicit conversion (in unsafe contexts only of course) from Span<T> to T*. This could technically break overload resolution, but i wonder if that would actually happen/matter in practice...
The above comments make sense and I agree that returning different types between unsafe and safe probably isn't the best idea, however, as mentioned by @CyrusNajmabadi, maybe changing stackalloc to always return a Span<T> with an implicit cast to a T*. This would still provide the ability to work with pointers if wanted but encourage the use of the safer Span<T> whilst also solving the original problem of returning a T* in a safe context.
Converting a Span<T> to a T* is not a safe operation. Span<T> has different layouts on different platforms, may be a GC tracked reference, and wouldn't be free from a codegen perspective.
stackalloc is often used in perf critical paths and I dont expect users would find it acceptable if that regresses.
I also think it adds more confusion for relatively little benefit. Checked/unchecked operations are bad enough and I wouldn't want to remember that var x = stackalloc might differ due to a non-visible unsafe keyword that is on the type 50 lines up.
Currently, if you want to use stackalloc to allocate a Span you need to do one of the following
or this
var span = (stackalloc T[size]);
var span = _ = stackalloc T[size]; should work too but it doesn't. looks like a bug to me.
@tannergooding I agree with what you are saying, however, I still don't find it great to have a type infer to a pointer in a safe context. From my understanding, unsafe code was always supposed to be an explicit opt-in feature and whilst I understand that stackalloc was an unsafe only operation before the introduction of Span<T> it just feels like sort of goes against this philosophy.
Why would this be a breaking change?
int Foo() { var s = stackalloc int[5]; }
does not currently compile.
int Foo() { Span<int> s = stackalloc int[5]; }
does.
unsafe int Foo() { var s = stackalloc int[5]; }
Compiles of course, and would still (if this is accepted) infere int*. So the meaning of existing code would not change.
I agree it's kind of weird that the return type changes based on unsafe context, but why is it a "breaking" change? All it does it allow code that currently does not compile to do so, no?
I don't think it's technically a breaking change. It would be a very odd language semantic, but it's not technically a break.
@chrisaut It's not about it being a breaking change to allow this; it's that doing this makes the simple act of changing a block to unsafe in the future breaking. Consider the following if this was done:
struct S
{
// 50 lines down....
int M()
{
var span = stackalloc int[8]; // not unsafe, inferred as Span<int>
return span.Length; // valid
}
}
And then you add an unsafe modifier somewhere:
unsafe struct S
{
// 50 lines down....
int M()
{
var span = stackalloc int[8]; // unsafe context, inferred as int*
return span.Length; // oops! cant do .Length on int*
}
}
@Joe4evr that's why I don't think we'd do this, but that doesn't make this a breaking change. A breaking change changes the meaning of existing, compiling code.
Right. And I probably agree that the gain here would be small compared to the weird semantics. I was just trying to understand why this would be breaking.
It would still be nice to just use var in safe contexts, but yeah....
@Joe4evr does it not make more sense then to return a Span<T> for both cases as you can easily get a pointer to the first element in a Span<T> if you wanted to work with pointers.
This is obviously a breaking change though
for both cases as you can easily get a pointer to the first element in a Span<T> if you wanted to work with pointers.
stackalloc is often used in perf critical paths and I dont expect users would find it acceptable if that regresses. Getting a pointer outside a span also requires using a fixed statement which has its own minimal overhead and while MemoryMarshal has some APIs that can help, they aren't available outside of .NET Core.
I don't think it is worth a breaking change for an edge case scenario around saving 5 characters when typing 😄.
-- It's also worth noting that you can't return a Span in all cases. You can't use a pointer as a generic type parameter so Span<int*> x = stackalloc int*[10] is illegal while int** x = stackalloc int*[10] is not.
Can we legalize Function((T*) stackalloc T[] { ... }) ?