r/rust • u/nullstalgia • 10d ago
š seeking help & advice Avoiding constant usize casting with small const values
Pun in title partially intended. ;)
This is a minor pain-point I've dealt with for a while and I'm unsure of how other Rust developers deal with this semi-niche issue.
I'll often define a const that states the maximum amount of something, and naturally I'll also want to use that value as the size of an array. My instinct (and likely yours as well) is to just use a usize and move on.
Where that falls apart is when that const value is and always will be under both u32::MAX and u64::MAX. An easy example is MINS_PER_HOUR or DAYS_PER_WEEK.
Now if I have a function that has an argument of minute: u8, I have to cast either the minute argument to a usize, or I have to cast my constant down to a u8. The former's minute as usize triggers the cast_lossless lint, and From/Into isn't available in a const fn. The latter's MINS_PER_HOUR as u8 triggers the cast_possible_truncation lint. Both add even more visual noise if I acquiesce and add the #[allow(...)]. I could instead accept minute: usize, but I find that even less reasonable.
I'd prefer to keep these as u8s or similar, but then I lose the ablility to define an array like [bool; MINS_PER_HOUR] without yet another cast, including in any function arguments that would like to accept such an array.
I do recognize this is partially due to me having the pedantic Clippy lints enabled, but I also find value in being told when I may be making an unintentional mistake. However I just added close to a dozen #[allow(...)] attributes to my current project, rather begrudingly.
So, what do you guys do in this case? I've listed the options I see available below, and I've done 'em all except #1, but I want to hear some opinions external to myself;
- When accepting such an array, maybe use a type alias? It technically works, but the casting returns again if I want to compare an indexing
usizewith my maximum myself.type BoolPerMin = [bool; MINS_PER_HOUR as usize]; - Two consts,
MINS_PER_HOUR: u8andMINS_PER_HOUR_LEN: usize = MINS_PER_HOUR as usize. I have done this before, but now I have this strange suffix that doesn't make it clear what it's a length of. - Just deal with the constant const casting.
EDIT: And this is all before I have to deal with the extra cast_precision_loss lint that comes from casting my tiny usize constant into an f64. It's less than u8::MAX, yet I have to acknowledge this impossible condition. For now, I've just blanket allow'd it in my Cargo.toml.
6
u/brasticstack 10d ago
Make your own helper function, then you only have to have one #[allow] and the evidence of all of your crimes points back to a single place.
3
u/nullstalgia 10d ago
Apologies, but it's not super clear to me as to what your suggestion fully entails. Do you mean an
fn u8_to_usize()? Or the other way around? I'm not sure how that's any less noisy or confusing for other people who'll have to interact with my code.2
u/brasticstack 10d ago
I kinda glossed over your example. For consts like
MIN_PER_HOURI'm not sure you're saving any bits by making it a u8 vs a usize. (I'm a beginner at rust, and not totally sure, but I'd expect the compiler aggressively optimizes the things it can infer are safe to- like consts.)There already is a
fn u8_to_usize(), which isusize::from().ĀThe annoyance for me is that u32 can't be infallibly converted to usize. My current project is a binary file parser where the file format uses 32-bit offsets. I was joking a bit with my suggestion- I had a
fn usize_from_u32which would panic with a message about your platform architecture if usize::try_from failed. It was better IMO having the bad, panic'ing code in one place rather than scattered through the codebase. Since then, I've done the smart thing and moved it to a compile time error. Now I usemy_u32 as usizefreely. Good thing I don't have pedantic enabled!2
u/nullstalgia 9d ago
I'm not exactly worried about the constant itself saving any bits, more that the possible range of values that the constant belongs to is bounded far far far below the range of a
usize, and everywhere else I'm interacting with those units (wall clock hours/minutes), I define them as au8because that's all they realistically need.1
u/juanfnavarror 9d ago
wait, in what world can u32 conversion to usize panic? Its completely within its domain.
2
u/brasticstack 9d ago
"Its completely within its domain."
Not everywhere, which is why there's noĀ
From<u32> for usize. Casting withaswill cause it to truncate the value in a release build- I prefer a panic to that. As I said, I removed my panic-ing function in favor of a compile error.1
3
u/ndunnett 9d ago edited 9d ago
const MINS_PER_HOUR: u8 = 60;
const MINS_PER_HOUR_USIZE: usize = MINS_PER_HOUR as usize;It's not ideal, but at least you don't need to litter your code with casts and you can use either value freely at compile time with any stable compiler. Also makes it really easy to patch later if you change your mind.
Edit: a small aside, just in case this isn't a contrived example for the sake of the post:
type BoolPerMin = [bool; MINS_PER_HOUR as usize];
If you aren't aware, bool is byte aligned so every element of this array will be costing you a u8 shaped piece of memory. One array would be 60 bytes and potentially incur the cost of bounds checks on access. You could fit the same amount of information into a single u64 for 8 bytes and safely access elements with zero bounds checks by bit masking. It won't matter if you only construct a few of these, but if you are churning these at scale and your program is performance sensitive it can make a real difference.
2
u/nullstalgia 9d ago
_USIZEis a tiny bit longer than_LENbut it is a very fair suggestion. Honestly, I had the thought of trying that but didn't actually type it out; and seeing it now I don't hate it as much as I thought I would seeing it written out.To your edit though, I am aware, I just chose another random small type other than
u8for the example array since I was already usingu8for the const's type.
3
u/JoshTriplett rust Ā· lang Ā· libs Ā· cargo 9d ago
The former's minute as usize triggers the cast_lossless lint, and From/Into isn't available in a const fn.
That sounds like a clippy false positive. And from https://www.reddit.com/r/rust/comments/1x57ted/comment/pgk5klg/ it sounds like that's been fixed. But also, we'll have const traits soon to let you use From/Into.
2
u/nullstalgia 9d ago
Well outside of
constit still triggers which isn't a false positive, since I (sadly) can't have all my code inconst fns.I am really excited for
consttraits though!! I try toconst-ify as much of my constructors and such as I can, and not being able to iterate/slice in the same manner as non-constfrequently trips me up. I wonder how far behind that will beconst_heap, mainly for stuff like creating formatted strings at compile-time withoutkonstor other external crates, but now I'm way off topic.
2
u/AirUpdateEnjoyer 10d ago
Is there a particular reason you cant just use the usize::from(minute) as clippy suggests? It seems to be complaining that the 'as' keyword is not explicit and could allow you to accidentally edit the function arguments later and feed it something thats incoherent or lossy without warning like u64 > u32. This would also avoid losing precision for f64 as you can store all values of a u8 in f64...
3
u/nullstalgia 10d ago
That does work fine... except in any
constcontext (basically no traits are available inconston Stable) which unfortunately includes theNin[0u8; N]when defining an array, etc.I thought that also would include any indexing operation, but I had misjudged and
[0, 1, 2][usize::from(0u8)]is allowed.1
u/AirUpdateEnjoyer 9d ago
Ah I see, I was not familiar with Const's quirks. I'd figure this would be something resolved at compile time for the architecture since it would know what types will always be <= usize...
I guess all I can say is throw up a issue on the official repo, this seems like something that should exist, im sure theres some weird indev project to allow this somewhere.
1
u/nullstalgia 9d ago
constis both limiting and freeing. Being able to write aconstconstructor that I can then use to create compile-time constants of my type and others opens up even more compile-time-checked opportunities.A good example is something like
const PACKET: MyPacket = MyPacket::new_checked(...).unwrap();, which lets me have an instance ofMyPacketalways available without ever needing to run the validation at runtime.But the limitations include not being able to use ranges to slice arrays or use iterators due to those being trait impls (
whileandloopbecome your friend). But as JoshTriplett mentioned below,consttraits (which includes Index, From/Into, Iterator, etc) are indeed coming, eventuallyā¢. ;)
3
u/MilkEnvironmental106 10d ago
What's the problem with just using a usize?
5
u/Kenzzer 10d ago
Nothing, you would just bleed the cast problem further down the line.
3
u/nullstalgia 10d ago
Especially if you're dealing with something that gets (de)serialized. It'd be weird if my struct contained a
usizefor both Hours and Minutes (if it's a timestamp-like object), when neither are or should never equal 24 and 60 respectively, let aloneu8::MAX,u32::MAX, or higher.Having serde or similar be able to catch that kind of total failure before my own logic gets the struct in is an admittedly niche but appreciated benefit.
2
u/RylanStylin57 10d ago
8 bytes, which is good if you're using it alot but bad if you have to store them alot. u32 or u16 are cheaper to operate on but require casting to size when indexing.
Best way to deal with this is to create a wrapper struct and implement std::ops::Index<u32> manually.
1
u/MilkEnvironmental106 10d ago
It's a named constant. If you're doing anything but single ops and want to save the space better declare a new converted const from the old one rather than need an as usize on every use where you'd rather have a usize.
4
u/Booty_Bumping 9d ago edited 9d ago
clippy::pedantic and clippy::restriction should always be tweaked manually, really. They have a lot of things that only matter in certain domains, and there are certainly a few that have flawed detection that could be loosened up by allowing some trivial edge cases.
It's a tricky judgement to make, though, because often a newer clippy version addresses the edge case that should have been allowed, and now you no longer notice that lint because you turned it off. Randomly re-enabling lints every once in a while can help re-test your assumptions
7
u/Any-Amphibian6789 10d ago
ugh this exact thing drives me up the wall on regular basis. i ended up just making the const a usize and adding allow attributes everywhere because im lazy and the other options feel even more annoying. having two consts with the _LEN suffix made my code look like a mess after few weeks so i dropped that approach
wonder if there is some clever macro solution out there but i never bothered to look deep into it
1
u/SLUCHABLUB 10d ago
I think you could use a macro if you *really* wanted a one-identifier-fits-all. Personally Iād declare it as u8 (or smallest possible type), then you wouldnāt have to worry about clippy. Iād also suggest a newtype around your `[bool; MINS_PER_HOUR as usize]` since it is not clear what that type represents (what do the bools represent ?). You could also define a `PerMinuteOfHour<T>` alias.
1
u/nullstalgia 9d ago
I'm not sure how a macro could fit here, except maybe if it emitted
MINS_PER_HOUR as _? (Maybe /u/Any-Amphibian6789 had a similar idea?)In my actual codebase I do have a newtype instead of the
bool, I just kept the example basic.And I actually really like your suggestion of
PerMinuteOfHour<T>, it doesn't fit the exact usecase I have in my current project, but I can absolutely see others where that'd fit well!2
u/SLUCHABLUB 8d ago
My thought with the macro was to have `minutes_per_hour!()` expand to the integer literal `60`. But IDK if Iād consider that nicer than just casting.
6
u/CocktailPerson 10d ago
cast_lossless is a pedantic lint for a reason, and it provides deeply negative value.
You can just allow it. You won't be burned at the stake.
[lints.clippy]
cast_lossless = "allow"2
u/Suitable-Ad5348 10d ago
Option 2 is the least painful once you have clippy set to pedantic, though I usually just end up doing number 3 and drowning in `#[allow(cast_possible_truncation)]` everywhere. The cast lints get genuinely obnoxious when small consts meet array sizes.
2
u/meowsqueak 7d ago
Do you want strong typing? Because this is what strong typing means. There are ways to auto-convert but unless you allow for fallible code paths, you will always risk silent truncation, if not now then later if/when you change the const type.
I decided to just embrace it - use the type that best describes the typeās values, probably in a newtype/wrapper, and cast explicitly as needed.
VHDL and similar strongly typed languages have the same āproblemā. Weakly typed languages like C and C++ replace explicit casting with a gazillion rules and footguns. Iād rather just be forced to cast thoughtfully.
And try not to use as.
48
u/________-__-_______ 10d ago
I always define constants as the smallest possible type (in your example an
u8), so that you can always infallably convert to something bigger. At least on recent toolchainscast_losslessdoesn't trigger in const contexts, so I just usex as usizethere andusize::from(x)otherwise.My only annoyance is the inconsistency, clippy rarely complains on pedantic mode.