Generics/bounds for numeric types


#1

Hello guys, I’d like to have generic numeric algos that’d work on arrays of numeric types.

A common operation is to index the arrays using the numeric values.

I can’t find a generic way to put a bound that’d force the generic type to be suitable for indexing arrays, so I’m getting errors if I tried to use it “as usize” or if I try to use it as index

Example, for this struct:

    struct TestStruct<T> {
        values: Vec<T>, // expected to hold integer (e.g. u8, i32, u64, etc) 
        count: usize,   // keep track count
    }

I’d like to have an implementation new function that would initialize the items array using a 0 to ‘n’ range:
pub fn new(n: usize) -> TestStruct { .. init code here ... }
or
pub fn new(n: T) -> TestStruct { .. init code here ... }

I can’t get either to work, example:

    for i in 0..n {
        values.append(i); // i is usize, can't append as values are type T
        values.append(i as T); // can't transmute usize to generic T
    }

How can I put bounds on my implementation to allow some usize <-> T conversions, preferably with features already built into Rust (instead of providing my own additional “usize convertible” trait for each target numeric type)


#2

This is full snippet (https://play.rust-lang.org/?gist=9a5a7ac44c008b29c2505bd8e84b1ed9&version=stable&backtrace=0):

#[derive(Debug)]
struct TestStruct<T> {
    items: Vec<T>, // parent link
    count: usize,   // number of components
}

impl<T> TestStruct<T> {
    pub fn new(n: usize) -> TestStruct<T> {
        let items = Vec::new();
        
        for _ in 0..n {
            let v = n as T;
            items.push(v);
        }
        
        TestStruct {
            items: items,
            count: n,
        }
    }
}

fn main() {
    let t: TestStruct<u16> = TestStruct::new(12);
    
    println!("{}", i32::min_value().abs());
}

And the failure:

rustc 1.13.0 (2c6933acc 2016-11-07)
error: non-scalar cast: `usize` as `T`
  --> <anon>:12:21
   |
12 |             let v = n as T;
   |                     ^^^^^^

error: aborting due to previous error

In this case T is actually a scalar.


#3

That’d be std::ops::Index (for non-mut, at least ), but the Implementors section shows that really the only implementor for slices is usize (or ranges thereof), so it probably won’t help you.

Then you’d think you want T:Into<usize>. But if you look at From (the usual way to implement Into), you’ll see that std is paranoid about usize, forcing you to code as though it might be only 8 bits even when you’re targeting something where it’s 64-bit.

You could use nightly and TryInto and handle the Results, but you’d probably be better off just making your own IntoIndex trait assuming you’ll be using reasonably-sized arrays and normal hardware.

For the constructor, though, you could just go through u64 if you know it won’t truncate:

impl<T:Clone+Into<u64>> TestStruct<T> {
    pub fn new(v: T) -> TestStruct<T> {
        let n: u64 = v.clone().into();
        let n = n as usize;
        let items = vec![v; n];
        
        TestStruct {
            items: items,
            count: n,
        }
    }
}

#4

There’s also the num-traits crate with trait FromPrimitive and fn cast.


#5

Actually @scottmcm I started with your proposal, creating my own IntoIndex, but at some point I ended up duplicating so much stuff that is available in the num_traits crate as suggested by @cuviper, I added the Num and NumCast bounds to get started, in practice for next level operations I’m needing Ord and Clone trait bounds too.

Well it’s done.

But being new to Rust it feels to have so much overhead (as of having to download an external package) for a such simple numeric casting and indexing operations.

What’d prevent Rust just for facilitating the “as” casting operation on generic definitions? possible having a built-in “Cast” like trait?


#6

A lot of things seem simple, but are complex to get right in a form that will be stable in the stdlib for basically forever - or they might require more language features to be done the right way. Therefore it’s Rust’s policy to let these features grow and mature in non-stdlib crates first.

Also with Cargo, don’t forget that the overhead of using an external package is “add two lines to your project”.


#7

as is very much like C’s cast operator in that it’s sometimes safe, and sometimes loses information. I already don’t like that, so am glad that there’s no trait for it.

In generics, From/Into handle the “always safe” conversions. It’s just that usize is nuanced enough that defining “always safe” is awkward, so it’s only defined in the uncontroversial case. For fallible conversions, TryFrom/TryInto are coming: https://github.com/rust-lang/rfcs/blob/master/text/1542-try-from.md


#8

Please remember that “safe” has specific meaning in rust-world: as is always safe, as is From/Into and also TryFrom/TryInto.


#9

Doesn’t that “safe” exclude undefined behavior? Some floating point casts are UB: #10184 #15536.

But otherwise, yes, the “safety” of casting integers to/from usize is only about overflow.


#10

Thanks for the reminder! Definitely a critical distinction.

For future readers, please substitute “lossless” in place of “safe” in my previous post.