Typecast between structs?

Hi experts. How can I cast struct T to struct U with as, similar to

let a: u32 = 123;
let b: f32 = a as f32;

I want to convert Celsius degree to Fahrenheit degree:

struct Celsius(f64);
struct Fahrenheit(f64);

fn main() {
    let c = Celsius(37.3);
    let f = c as Fahrenheit;   // how to make it possible?
}

In C++, I can make it with the following

Fahrenheit::Fahrenheit(const Celsius& c) const {...}
Celsius::Celsius(const Fahrenheit& f) const {...}
/*
int main()
{
    Celsius c(37.3);
    Fahrenheit f1(c);
    Fahrenheit f2 = (Fahrenheit)c;
    Fahrenheit f3 = c;
    ...
}
*/

Thanks in advance.

I recommend you check out the uom crate.

1 Like

Syntactically, this is not supported. Semantically, this is usually done either with dedicated methods, or by implementing From trait.

6 Likes

The Rust equivalent of C++'s conversion constructor is the From trait.

You could define Celsius and Fahrenheit like this:

#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
pub struct Celsius(f64);

impl From<Fahrenheit> for Celsius {
    fn from(f: Fahrenheit) -> Celsius {
        Celsius((f.0 - 32.0) * 5.0 / 9.0)
    }
}

#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
pub struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Fahrenheit {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

And you then invoke the conversion explicitly (e.g. Fahrenheit::from(celsius)) or using the into() method from the Into trait that you get for free whenever From is implemented.

fn main() {
    let c = Celsius(0.0);

    // You can explicitly use the Fahrenheit::from() to do the conversion
    let f = Fahrenheit::from(c);
    assert_eq!(f, Fahrenheit(32.0));

    // Or you can use the into() method and let type inference figure out which
    // conversion to use
    let equivalent_celsius: Celsius = f.into();
    assert_eq!(equivalent_celsius, c);
}

(playground)

You can think of the as keyword as a tool for coercing between different primitive types, so bool -> u8, f32 -> u64, *const Foo -> *mut Bar, and so on, and is deliberately not extensible because it'll do the corresponding compiler magic. The Rust Reference has a chapter on type cast expressions if you want to find out more.

8 Likes

Is Rust equivalent of C++'s parameterized constructor the From trait as well?

For example, in C++:

Celsius::Celsius(double value)
{
    if( value < -273.15)
        m_value = std::numeric_limits<double>::quiet_NaN();
    else
        m_value = value;
}

In Rust:

impl From<f64> for Celsius {
    fn from(value: f64) -> Celsius {
        if value < -273.15 {
            Celsius(f64::NAN)
        } else {
            Celsius(value)
        }
    }
}

However, let c: Celsius = Celsius(-275.0) does not run into the function defined in the From trait. How can I make it similar to Celsius c(-275.0) in C++? Or how can I prevent users from calling let c: Celsius = Celsius(-275.0)?

Privacy. Don't mark the field as pub and only offer ways to construct or modify that uphold your invariants.

7 Likes

Thank you. That works.

Base on @quinedot's answer, you can also consider return Option<Self> from fn new() so all existing Celsius are valid temperature. Like this one.

1 Like

Normally you'll use the TryFrom trait for fallible conversions because it lets you return a descriptive error when the conversion fails.

Otherwise you might create a named constructor if it's an "interesting" conversion or the meaning may be ambiguous (e.g. String::from_utf8()) .

5 Likes

You should avoid using as conversion actually.
Using into() and try_into().unwrap() is better because they fail conversion instead of providing invalid value.
E.g.:

let a: u32 = 100000;
let b: u16 = a as u16; // Silently corrupts data

let b: u16 = a.into(); // Doesn't compile
let b: u16 = a.try_into().unwrap(); // panics at runtime

let a: u8 = 5;
let b: u32 = a.into(); // OK

let a: u32 = 5;
let b: u8 = a.try_into().unwrap(); // OK
4 Likes