5 ms·
> [unsigned] extends the range of valid values versus signed types, which is sometimes useful When is it useful? On a 32 bit platform, for file offsets, maybe
by xscott 5y ago
> [unsigned] extends the range of valid values versus signed types, which is sometimes useful
When is it useful?
On a 32 bit platform, for file offsets, maybe that last bit was useful for a few years. But as soon as disks got bigger than 4GB, you needed to switch to a 64 bit `off_t` and `lseek` anyways.
For array indexes, on a 32 bit platform, there is really only one VERY CONTRIVED case where that last bit for `unsigned` matters: your program creates a single array of bytes greater than 2GB. As soon as it's an array of 2-byte shorts or anything larger, you never need that last bit. And the configurations where a 32 bit kernel lets a program use more than 2GB of address space were not super common - it was better to switch to a 64 bit platform at that point.
On 64 bit platforms, you never need that last bit for array indexes or file offsets because no system has 10,000 petabytes of memory or 10,000 petabyte file sizes. And they won't for a while. Unless clock speeds get back on Moore's law, that for loop is going to take a long time to run anyways...
> Writing loops like this is often a code smell.
One person's code smell is another person's daily idiom. I think signed array indexes are like complex numbers - people who don't need them can't imagine why anyone else does. (https://github.com/golang/go/issues/19921 https://github.com/golang/go/issues/19921)
For me, I frequently do math on the array index. Reverse loops are necessary sometimes (I can give examples), but let's ignore that for now. How would you write this simple loop with unsigned loop indices?
double scale = <non-integer value>
for (ssize_t ii = 0; ii<len; ii++) {
data[ii] = sinc(scale*(ii - len/2));
}
Note that `len/2` using integer division (truncating) is doing the right thing for both even and odd lengths. I really want that `sinc` function centered on a specific sample.
If either `ii` or `len` is unsigned, C/C++ quietly does something awful here. Stuff like this makes me resent the STL for choosing size_t over ssize_t.
I think it's ugly in Rust too:
let len = data.len(); // usize because that's the way
for ii in 0..len {
data[ii] = sinc(scale*(ii as isize - len as isize / 2) as f64);
}
Thankfully the `as` operator binds pretty tightly.
I don't think there are any good reasons to use unsigned integers for array indexes or sizes. It doesn't help with bound checking (it's the same assembly to check for unsigned out of bounds as it is to check for signed out of bounds or less than zero). It doesn't help with "large" arrays (except in one very contrived case). And it gets in the way when you need to do math on the indexes.
- tialaramex 5y ago(In Rust) the ugliness for me stems from the fact we got this array from somewhere and now we're overwriting it, and I start to think probably the code should actually make the data structure instead, whereupon the need for these to be indices goes away and it's some sort of iterator into collect ?
- xscott 5y agoI see your point, but I've got some practical objections to it: a) You can't (sanely) initialize a fixed-sized rust array with an iterator. It looks like one of the following: let a = [1.0f64, 2.0, 3.0]; // not convenient for large arrays let a = [1.0f64; 1000]; // large array, but only the same value I did find an insane example using an unsafe block. I've got no moral qualms about unsafe blocks, but using them to initialize an array seems heavy. b) If you want it dynamically sized, you're probably going to use `Vec` and maybe an iterator as you suggested. I didn't try to compile the following, but I don't think it looks better than the for loop: let len = get_command_line_argument() as isize; let a: Vec<f64> = (0..len).map(|ii| sinc(scale*(ii - len/2) as f64)).collect(); You could break it down with more .maps, but I think it'd be even worse to look at. I think it's also growing iteratively (amortized constant, but still reallocating and copying along the way). c) There are obvious cases where you want to re-use rather than reallocate your arrays. In addition to the cost of allocation and initializing to the wrong value, there are other times you might like to re-use the existing array address (FFI, etc..). You'd probably want to keep it in a Box<[T]> instead of a Vec<T>, but the point stands. For (b), I suppose you could write a helper function: let a = make_vec(len, |ii| sinc(scale*(ii - len/2) as f64)); That's not awful. Both len and ii can be isize, and the implementation of make_vec can reserve the size in advance. Maybe I'll add something like that to my toolbox :-)
- tialaramex 5y ago> I think it's also growing iteratively (amortized constant, but still reallocating and copying along the way). Nah. Let's see why that isn't the case because it's instructive: 0..len is a Range, Range implements core::iter::TrustedLen which is an unsafe trait that says "I promise" (hence it must be unsafe) "that my size hint isn't a hint at all but my actual exact size". [ Rust's "built-in" containers and iterator adaptors eagerly implement TrustedLen, you should consider doing likewise if you make things that you're absolutely certain know their correct size and wouldn't otherwise be TrustedLen ] Because Range implements TrustedLen the Map also chooses to implement TrustedLen since it doesn't change the length and can just pass along what Range said. The iterator.collect() call effectively ends up as spec_extend(iterator) on the Vec, and spec_extend() for an iterator with TrustedLen uses the size hint to reserve() the appropriate amount of space in the vector up front.