Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicit signedness casts for integers #359

Closed
Rua opened this issue Mar 26, 2024 · 14 comments
Closed

Explicit signedness casts for integers #359

Rua opened this issue Mar 26, 2024 · 14 comments
Labels
ACP-accepted API Change Proposal is accepted (seconded with no objections) api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api

Comments

@Rua
Copy link

Rua commented Mar 26, 2024

Proposal

Problem statement

Casting using the as operator has some issues, in that it does not necessarily reflect the intent of the cast. Was the cast meant to be lossy or not? Was it meant to preserve the size? Constness? Signedness?

Some of the motivations for pointer constness casts raised at https://internals.rust-lang.org/t/casting-constness-can-be-risky-heres-a-simple-fix/15933 would apply here too. When refactoring, the sizes of types may change, and silently introduce bugs due to casts not being updated.

Clippy has the cast_possible_wrap and cast_sign_loss lints, which tag sign-changing casts, but since there is no alternative to using as, it cannot propose a fix.

Motivating examples or use cases

Rust has been slowly introducing alternatives to direct casts, to more clearly signal intent, catch errors, and allow casting only one aspect of a type instead of everything in one go. From is used when the cast is lossless, TryFrom for checked casts, ptr::cast_const and ptr::cast_mut change the mutability and nothing else, ptr::cast changes pointed type and nothing else.

Solution sketch

Following the example of ptr::cast_const and ptr::cast_mut, I propose adding two new methods to integer types, for all X including size:

  • uX::cast_signed() -> iX
  • iX::cast_unsigned() -> uX

These do the same as a regular as cast, but crucially they only cast to another integer of the same size and opposite signedness. They don't ever change the size, so i32u64 and u64i32 are not implemented.

Alternatives

Other names for the two proposed methods would be possible, such as:

  • reinterpret_*, following the example of C++.
  • transmute_*, since this is really a kind of transmute.
  • cast_sign for both methods. This has the downside of not conveying which direction you're casting in, which the pointer constness methods do indicate.
  • iX::from_bits and iX::to_bits, following the example of floats. The methods would only be present on signed integers in this case.

It would also be possible to implement this as a trait, like the explicit_cast crate does, but since it's a closed class of types, that seems overkill. There have also been proposals to include this under a more general "wrapping cast" or "lossy cast" function/trait of some sort, but I feel that a i32u32 cast isn't in the same class of casts as i32 -> i16, since the former is fully reversible and the latter is actually lossy.

Links and related work

@Rua Rua added api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api labels Mar 26, 2024
@Rua Rua changed the title (My API Change Proposal) Explicit signedness casts for integers Mar 26, 2024
@aapoalas
Copy link

Bikeshed: These should definitely follow fX::from_bits as they're the exact same bitwise "conversion"/reinterpretation operation on just a different numeric type.

@Rua
Copy link
Author

Rua commented Mar 26, 2024

Bikeshed: These should definitely follow fX::from_bits as they're the exact same bitwise "conversion"/reinterpretation operation on just a different numeric type.

The downside is that it makes method chaining a bit uglier with from_bits than it would be with cast_signed.

@BurntSushi
Copy link
Member

What is the intended behavior of (-1i32).cast_unsigned()?

@GrigorenkoPV
Copy link

What is the intended behavior of (-1i32).cast_unsigned()?

u32::MAX I suppose. From what I can tell, this is meant to be a bitwise-cast, and Rust guarantees two's complement.

@Rua
Copy link
Author

Rua commented Mar 26, 2024

What is the intended behavior of (-1i32).cast_unsigned()?

u32::MAX I suppose. From what I can tell, this is meant to be a bitwise-cast, and Rust guarantees two's complement.

Yes. It's exactly the same operation as -1i32 as u32, but made more explicit.

@aapoalas
Copy link

Re: bikeshedding on from_bits

It'd be bad if the answer to the question of "how do I reinterpret 64 bits as a numeric type X?" depended on X. It has a good chance of spawning a mnemonic: "from for floats, reInt for ints" or something similar.

@Amanieu
Copy link
Member

Amanieu commented Apr 2, 2024

We discussed this in the libs-api meeting today. We're happy to add these methods.

Feel free to open a tracking issue and open a PR to rust-lang/rust to add it as an unstable feature.

@Amanieu Amanieu closed this as completed Apr 2, 2024
@jhpratt
Copy link
Member

jhpratt commented Apr 4, 2024

Trait methods take precedence over inherent methods nowadays, correct? Trying to determine if there is any future breakage for num-conv, which has an identical API using extension traits. To be clear, I have no issue whatsoever with breakage and would love to see the API.

@cuviper
Copy link
Member

cuviper commented Apr 4, 2024

Trait methods take precedence over inherent methods nowadays, correct?

Only while the inherent method is unstable, otherwise it takes precedence.

@jhpratt
Copy link
Member

jhpratt commented Apr 4, 2024

Good enough for num-conv, given that the signature and behavior are identical.

@ChayimFriedman2
Copy link

I'm worried about the fact that the names cast_(un)signed() doesn't express the fact that they don't check for overflow. This makes them almost as bad as as IMHO. Maybe cast_(un)signed_wrapping()?

@GrigorenkoPV
Copy link

This makes them almost as bad as as IMHO.

Not really. The main problem with as (at least as I see it) is that it does too many things. References and pointers aside, just with numbers it can do

  • a lossless conversion to a wider integer type,
  • a truncating conversion to a narrower integer,
  • a wrapping/bit-wise conversion to the opposite signedness,
  • a conversion to and from a floating point number, with potential loss in precision, not to mention INFs, NANs, and other tasty stuff.

@Rua
Copy link
Author

Rua commented Jun 2, 2024

We discussed this in the libs-api meeting today. We're happy to add these methods.

Feel free to open a tracking issue and open a PR to rust-lang/rust to add it as an unstable feature.

Does this need an RFC first, or can I just link to this issue?

@jhpratt
Copy link
Member

jhpratt commented Jun 2, 2024

Linking to this issue is sufficient.

@Amanieu Amanieu added the ACP-accepted API Change Proposal is accepted (seconded with no objections) label Oct 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ACP-accepted API Change Proposal is accepted (seconded with no objections) api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api
Projects
None yet
Development

No branches or pull requests

8 participants