Is there a way to allow indexing `Vec` by `i32` in my program?

I implement solutions for algorithmic challenges which usually require a lot of work with Vec. It is because Vec allows indexing by usize only, my program is full of [some_i32_var as usize] casts. Is there a workaround?

1 Like

May be I am missing something, but why not use usize instead of i32 in your solution?

Im not sure if it's a good idea, but you might be able to implement Index and IndexMut Index in std::ops - Rust for i32. Something along the lines of

impl Index<i32> for Vec {
    fn index(&self, index: i32) -> Output {
        self[index as usize];
    }
}

You won't be able to do this. The "orphan rules" make it so you can only implement a trait for a type if either the trait or type are defined in your crate. Otherwise it'd be trivial for me to monkeypatch your std::process::Command to do rm -rf / :stuck_out_tongue_closed_eyes:

You can work around this by defining a transparent wrapper around Vec which will let you define whatever methods and trait implementations you want on it.

This is just fixing the symptoms though, the root problem is you are trying to index a vector by a signed integer. The standard library never allowed this (by implementing Index<32>) because half of the integer values are invalid and would result in runtime panics... It's a pain, but I think of the verbose casts as a gentle reminder from the compiler of every point you can stuff up indexing if you accidentally count down to -1 instead of 0.

Are there any particular reasons why you need to index by i32 instead of usize?

2 Likes

To put this another way, if your program currently doesn't panic, then you must never be using negative indices. In that case, maybe your value should be signed, allowing the compiler to enforce that fact.

I do not fully understand the security consideration. Implementing a trait for a particular type would only change behaviour of that type in my own program. Why does it considered a vulnerability?

No. It may just be a habbit to use int for all in Cpp solutions.

Signed/unsigned is not really a source of problem. I might be using u8 and still not able to index Vec with it.

I probably don't fully understand the orphan rule, but as far as I understood it, it was also a way to avoid ambiguity and breaking changes.

If Rust allowed you to impl Index<i32> for Vec in your own crate - what if the next version of the Rust standard library added its own impl Index<i32> for Vec which did something completely different? Which one would your crate now use?

I believe upcoming specialization work might have implications for this, but I'm not familiar with any of it.

1 Like

This is true. The broader point though was that if your number is an index, then it should be a usize. That's more or less what the type system is there for. The cast is the price you pay for converting whatever quanity you have into an index.

That said, as far as I'm aware, there's no particular reason why Vec shouldn't be indexable by any unsigned type (other than the complexity it would add implementing it).

2 Likes

I agree. My be I should think of indices as not just a numbers. I'll try.

2 Likes

Is there a RFC or discussion for adding implementations of Index and IndexMut for other numeric types? I've seen only this. For unsigned types it's logical to be able to index slices (u64 on 32-bit machine should be covered by usual checks), and for signed we can just panic on negative values. In some my programs I have a noticeable amount of as usize boilerplate (I work with u8 and u16 indexes to save memory), which I would be happy to remove.

1 Like

For what it's worth, you can get around the orphan rule by wrapping Vec in a "newtype" like struct MyVec<T>(Vec<T>) and then implementing the traits you want for that type instead.

Yes, but then you should always use my_vec.0 for acessing internal Vec:

let mut v = MyVec::<i32>(vec![]);
v.0.push(2);

which is even worse than using as usize :frowning:

If you implement Deref and DerefMut, method calls should transparently drop down to the inner Vec.

1 Like

For those interested, I implemented a proof-of-concept demo program, which allows MyVec indexing with i32: CODE HERE.

MyVec is still lack some Vec features. E.g. it can not be iterated using for i in &my_vec.

There is a reason why you should use an unsigned type here (See the answer from Michael!)
You just cast the i32 to usize which will do very strange things when you try to Index a negativ value...
I will repeat: DON'T DO THIS
for Real, stick to the given API, it was designed on purpose, so there are many hours of thinking and implementing involved.

My advise is: don't use the i32 in first place. Go the Rust way, not the C++ one.

5 Likes

To this, I would add that unlike in C/++, Rust's signed and unsigned integers should have identical performance characteristics because both are defined to wraparound on overflow (wheras in C/++ signed integer overflow is UB). So there is no reason to pick signed over unsigned when the problem domain does not dictate so.

3 Likes

This was not a UB warning, but a warning for a panic (because it wraps and so the Index will panic and he may doesn't know why)
In this case there is a dedicated problem domain namely, negative indexing is not plausible!

How is the negative indexing principally different from trying to index value outside of the vector/slice? In both cases we'll just panic. Meanwhile the recommendation "just use as usize" can lead to unexpected results.

1 Like

Let's assume you have a function that will do fancy stuff with a number. But for all numbers less than 0, it will produce some garbage.
Why you should use a signed type here? Right, you shouldn't. You are eliminating approx 2x10e9 values which will produce garbage at compile time! This is what you always should prefer, no matter what the language is (even in C++, Java, Go, ...). I you define something that will only work with positive numbers, just take an unsigned type. Simple as that.

Let me explain it further. What @907th does atm is &self.0[index as usize] which will convert e.g. -1 to 18446744073709551615. Good luck finding that number in your code, when it panics. You simply can't, because it isn't there.

1 Like