Are casts encouraged in Rust?
Are casts encouraged in Rust?
Posted Jun 30, 2025 22:56 UTC (Mon) by alx.manpages (subscriber, #145117)In reply to: Are casts encouraged in Rust? by NYKevin
Parent article: How to write Rust in the kernel: part 2
This is an interesting discussion. You have a point, which I also made with someone else recently.
Your right in calling C's implicit conversions messy, but it's not true of all of them.
C has three types of implicit conversions:
- Integer promotions.
These trigger for any fundamental type narrower than an int. I've called them a "cancer" myself recently. They are there because of historical reasons. I would remove them from the language if I could, but of course we can't at this time.
It is bad that a uint16_t is promoted to an int on almost every situation, which even changes its signedness.
The good thing is that few people actually use narrow integers like short, int16_t, or uint8_t.
The better thing is that the new _BitInt(N) integers added in C23 don't have integer promotions: a _BitInt(16) will not be promoted to an int.
So, I'd say we've partially solved this issue in C. Although we're not over. We also need to be able to specify literals of such types. I've written a proposal for the C Committee (and an extension request to both GCC and Clang) for that:
<https://github.com/llvm/llvm-project/issues/129256>
- Usual arithmetic conversions.
When adding, comparing or otherwise using two different types of integers in an operator that takes two operands, these trigger.
So, if you have
int a = 42; long b = 7; if (a < b) return a;you'll get the usual arithmetic conversions to turn that int into a long. Since both retain the original signedness, this conversion is harmless, and doesn't trigger any diagnostics at all. This is a good conversion.
If you had that comparison between integers of different signedness, you could get a warning with -Wsign-compare or -Wsign-conversion (depending if you're comparing them or adding/multiplying/... them, but they're both essentially the same thing).
alx@debian:~/tmp$ cat c.c int main(void) { int a = 42; unsigned long b = 7; if (a < b) return 0; } alx@debian:~/tmp$ clang -Weverything c.c c.c:7:8: warning: comparison of integers of different signs: 'int' and 'unsigned long' [-Wsign-compare] 7 | if (a < b) | ~ ^ ~ 1 warning generated.and
alx@debian:~/tmp$ cat c.c int main(void) { unsigned int a = 42; int b = 7; if (a < b) return 0; } alx@debian:~/tmp$ clang -Weverything c.c c.c:7:8: warning: comparison of integers of different signs: 'unsigned int' and 'int' [-Wsign-compare] 7 | if (a < b) | ~ ^ ~ 1 warning generated.This is sadly not turned on on -Wall -Wextra, but this is one diagnostic that you'd usually want, and most of the times it uncovers subtle bugs.
I said "could", because that diagnostic is not always triggered. It triggers if there can be information loss. There's a case where there can't be information loss: the unsigned integer is turned into a wider signed integer type that can represent all of the values that the unsigned integer can hold:
alx@debian:~/tmp$ cat c.c int main(void) { unsigned int a = 42; long b = 7; if (a < b) return 0; } alx@debian:~/tmp$ clang -Weverything c.c alx@debian:~/tmp$
This is another good conversion you want to happen. It's good that we don't diagnose it. - And then there are implicit conversions as if by assignment.
The C standard describes all implicit conversions as if by simple assignment. These happen, for example, when you assign some integer to a variable of another integer type.
This can be a narrowing conversion, in which case you'll get a very explicit diagnostic:
alx@debian:~/tmp$ cat c.c int main(void) { long l = 42; int i = l; } alx@debian:~/tmp$ clang -Weverything -Wno-unused c.c c.c:5:10: warning: implicit conversion loses integer precision: 'long' to 'int' [-Wshorten-64-to-32] 5 | int i = l; | ~ ^ 1 warning generated.Again, this is not in -Wall -Wextra, but you probably want to turn on -Wshorten-64-to-32 (and similar ones) for your projects, and disable it only when you know those conversions are good.
I personally disable it selectively in a few places with
#pragma clang diagnostic ignored "-Wshorten-64-to-32"in a few places in a library where I know that's exactly what I want.
It can also be a sign-changing conversion:
alx@debian:~/tmp$ cat c.c int main(void) { unsigned int l = 42; int i = l; } alx@debian:~/tmp$ clang -Weverything -Wno-unused c.c c.c:5:10: warning: implicit conversion changes signedness: 'unsigned int' to 'int' [-Wsign-conversion] 5 | int i = l; | ~ ^ 1 warning generated.
which is covered by the same -Wsign-conversion I mentioned earlier, which you also want on all the time, with a few exceptions maybe.
---
So, the -Wall -Wextra compiler diagnostics are a bit lacking, but if you turn on all available diagnostics, they're quite safe. Rust's .into() seems like C's behavior when the diagnostics are on, except that it doesn't allow the few conversions that don't produce any diagnostic in C, and which are actually Good Conversions. Also, Rust's .into() is just typographic noise, because good conversions is what I want all the time.
---
Then there's the issue that Rust is unable to do .into() with constant expressions, which forces you to use casts. That's worse than not allowing the good conversions without noise; this is plain dangerous.
Posted Jul 1, 2025 1:03 UTC (Tue)
by NYKevin (subscriber, #129325)
[Link]
This is a matter of opinion. I want to do all my conversions at the system boundaries, and then use the proper types throughout the program (preferably full-blown structs and enums, not just raw i32 or whatnot), with minimal or no further conversions after data has been ingested.
Anyway, I think I figured out why Rust does not allow that. Unlike all the other binary operators, the shift operators do support arbitrary mixing of integer types. However, their documentation pages point out that Rust applies a special rule when doing type inference: In the expression a << b (or a >> b), if Rust knows that a and b are both integers (of some possibly-unknown type), then the type inference system is special-cased to infer that the shift expression has the same type as a.
That actually tells me a lot. First of all, if you don't have that rule, it must cause issues, probably because whenever b is ambiguous, type inference can't figure out which overload to use, and just gives up rather than trying each in turn. Trying each in turn would produce the same result in this case (type of the output is the same as type of the left operand), but Rust is not always willing to do that if it can't prove that there's a unique solution (Rust is not C++ and does not want to reinvent SFINAE etc.). But that in turn means that the special case has to be really simplistic, and in particular, it must be possible to apply with partial information - if you only know the type of a and not the type of b, the rule allows you to make progress, because it only depends on the type of a. If you only know the type of b, then the rule doesn't help at all, but at least it does something in the other case. Finally, if you know neither type, then you can at least try to unify the output type with the left operand's type, and maybe that will tell you something.
So, if we wanted to allow a + b with mixed types, and make the output type be the *wider* of the two (instead of the left operand's type), then we'd probably have to give up on this idea of special-casing the type inference machinery (there's no rule you can come up with that will allow making forward progress when one and only one of the types is known). That in turn would lead to whatever problems they were originally having with a << b (i.e. probably the compiler asks for way too many type annotations).
Are casts encouraged in Rust?