Cássio
Cássio

Reputation: 43

Non-scalar cast: `T` as `f64` when doing generic multiplication

I want to implement a trait for a generic-type struct. A method inside the trait block must return a non-generic type. I'm having problems while trying to cast:

struct Rectangle<T> {
    x: T,
    y: T,
    width: T,
    height: T,
}

trait Area {
    fn area(&self) -> f64;
}

impl<T> Area for Rectangle<T>
    where T: std::ops::Mul<Output=T> 
{
    fn area(&self) -> f64 {
        let t_area = self.height * self.width;
        let f_area = t_area as f64;
        f_area
    }
}

fn main() {
    let sq = Rectangle { x: 0, y: 0, height: 1, width: 1 };
    println!("{}", sq.area());
}

And the compiler output is:

error: non-scalar cast: `T` as `f64`
  --> src/main.rs:22:22
   |
22 |         let f_area = t_area as f64;
   |                      ^^^^^^^^^^^^^

Can I cast T to f64 without using unsafe blocks?

Upvotes: 3

Views: 2206

Answers (2)

Matthieu M.
Matthieu M.

Reputation: 300409

Can I, not using unsafe blocks, cast T to f64?

What's T? The code to convert u8 to f64 is certainly not the same as the code to convert u64 to f64 (the latter can fail, after all).

As I see it, you have two paths ahead of you:

  • make Area generic over T, and thus return a T
  • constrain T to types that can be converted into a f64

I'll demonstrate the latter:

impl<T> Area for Rectangle<T>
    where T: std::ops::Mul<Output=T> + Clone + std::convert::Into<f64>
{
    fn area(&self) -> f64 {
        self.height.clone().into() * self.width.clone().into()
    }
}

Upvotes: 7

Lukas Kalbertodt
Lukas Kalbertodt

Reputation: 89016

Can I, not using unsafe blocks, cast T to f64?

No, and there are many good reasons for that. T could be any type, so it doesn't need to be compatible with or similar to f64 at all. The only thing you know about your T is, that it implements std::ops::Mul<Output=T>. But that doesn't help, because that trait doesn't say anything about f64 either.

So what you could do, is to bound T to be std::ops::Mul<Output=f64>. This means that multiplying two Ts will result in an f64. Your function then works (even without the as f64 cast), but you make your impl way less generic.


The proper way to solve this problem, is to abstract over multiple kinds of numbers. Thankfully, the num-traits crate has already done that. In particular, you are probably interested in the ToPrimitive trait. Your impl should then probably look somewhat like:

use num_traits::ToPrimitive;

impl<T> Area for Rectangle<T>
    where T: std::ops::Mul
          <T as std::ops::Mul>::Output: ToPrimitive

{
    fn area(&self) -> f64 {
        let t_area = self.height * self.width;
        t_area.to_f64().unwrap()
    }
}

Minor problem: we have this unwrap() here which panics when the multiplication result can't be converted into an f64. I'm not quite sure why this can happen, but we need to be aware of that. Maybe there is a better solution without such an unwrap().


You can of course also create your own trait. Something like:

trait AsF64 {
    fn cast(self) -> f64;
}

impl AsF64 for i32 {
    fn cast(self) -> f64 { self as f64 }
}

// more impls for u32, u16, i16, ...

And then impl for <T as std::ops::Mul>::Output: AsF64.

Upvotes: 3

Related Questions