Casting bitstruct to wider base type
bitstruct Foo : ushort {
bool a;
}
fn void main() {
Foo foo;
// Currently
ulong a = (ulong)(ushort)foo;
// Potential improvement?
ulong b = (ulong)foo;
}
Are there any safety issues one should consider here?
Explicitly casting wider? Probably not. Explicitly casting to a narrower type is always a safety issue, but also it's already allowed for all the other types. I guess for an enum, with it being sequential and starting at 0, the likelihood of creating problems by casting away significant bits is lower than the other types, but that's about it.
Maybe there could be a rule that if the type is not inline and the cast is narrowing, actually require to cast to the base type (or wider) first? I don't know if that would improve anything though.
I've compiled a bit of an overview of the minimum required casts for myself. Two things that stick out is that bitstruct is
- the only type that requires this kind of cast and
- doesn't have the
inlineoption.
bitstruct Bitstruct : uint { bool a; }
enum Enum : uint { A }
enum Inline_Enum : inline uint { A }
enum Const_Enum : const uint { A }
enum Const_Inline_Enum : const inline uint { A }
typedef Typedef = uint;
typedef Inline_Typedef = inline uint;
fn void main() {
Bitstruct foo_bitstruct;
Enum foo_enum;
Inline_Enum foo_inline_enum;
Const_Enum foo_const_enum;
Const_Inline_Enum foo_const_inline_enum;
Typedef foo_typedef;
Inline_Typedef foo_inline_typedef;
ulong bitstruct_wide = (ulong)(uint)foo_bitstruct;
ulong enum_wide = (ulong)foo_enum;
ulong inline_enum_wide = foo_inline_enum;
ulong const_enum_wide = (ulong)foo_const_enum;
ulong const_inline_enum_wide = foo_const_inline_enum;
ulong typedef_wide = (ulong)foo_typedef;
ulong inline_typedef_wide = foo_inline_typedef;
ushort bitstruct_narrow = (ushort)(uint)foo_bitstruct;
ushort enum_narrow = (ushort)foo_enum;
ushort inline_enum_narrow = (ushort)foo_inline_enum;
ushort const_enum_narrow = (ushort)foo_const_enum;
ushort const_inline_enum_narrow = (ushort)foo_const_inline_enum;
ushort typedef_narrow = (ushort)foo_typedef;
ushort inline_typedef_narrow = (ushort)foo_inline_typedef;
}
I am not super happy about how you can accidentally narrow things today. For the ptr -> int conversion, you can't narrow without an extra step, but I suppose widening should be fine. With inline presumable narrowing with explicit cast should be allowed to?
What about this:
ulong bitstruct_wide = (ulong)foo_bitstruct;
ulong enum_wide = (ulong)foo_enum;
ulong inline_enum_wide = foo_inline_enum;
ulong const_enum_wide = (ulong)foo_const_enum;
ulong const_inline_enum_wide = foo_const_inline_enum;
ulong typedef_wide = (ulong)foo_typedef;
ulong inline_typedef_wide = foo_inline_typedef;
ushort bitstruct_narrow = (ushort)(uint)foo_bitstruct;
ushort enum_narrow = (ushort)(uint)foo_enum;
ushort inline_enum_narrow = (ushort)foo_inline_enum;
ushort const_enum_narrow = (ushort)(uint)foo_const_enum;
ushort const_inline_enum_narrow = (ushort)foo_const_inline_enum;
ushort typedef_narrow = (ushort)(uint)foo_typedef;
ushort inline_typedef_narrow = (ushort)foo_inline_typedef;
Yeah, maybe. I can see this making it a bit safer, for example when you have some code where you need the raw integer
uint a = (uint)foo_bitstruct;
but then later decide foo_bitstruct needs more bits, this now unintentional narrowing cast would go undetected.
What would the reverse look like then, like this?
ulong wide;
ushort narrow;
foo_bitstruct = (Bitstruct)(uint)wide;
foo_enum = (Enum)(uint)wide;
foo_inline_enum = (Inline_Enum)(uint)wide;
foo_const_enum = (Const_Enum)(uint)wide;
foo_const_inline_enum = (Const_Inline_Enum)(uint)wide;
foo_typedef = (Typedef)(uint)wide;
foo_inline_typedef = (Inline_Typedef)(uint)wide;
foo_bitstruct = (Bitstruct)narrow;
foo_enum = (Enum)narrow;
foo_inline_enum = (Inline_Enum)narrow;
foo_const_enum = (Const_Enum)narrow;
foo_const_inline_enum = (Const_Inline_Enum)narrow;
foo_typedef = (Typedef)narrow;
foo_inline_typedef = (Inline_Typedef)narrow;
Perhaps. It would be in line with having to do
char a = 123;
iptr b = 123;
void* x = (void*)(iptr)a;
void* x = (void*)b;
This is already required BTW. Same the other way around, you need to do (char)(uptr)&foo. Again, this is to protect from accidentally doing casts that silence errors you didn't expect.
For example in this situation:
// This works on 32 bits
// But it would be a compile time error in
// current C3 on 64 bits
uint x = (uint)&a;
void* y = (void*)x;
This would be what would be needed to cast to uint on 64 bits for C3 today:
uint x = (uint)(uptr)&a;
void* y = (void*)(uptr)x;
And I think this makes sense, even though it's a bit ugly.