DEV Community

Tim McNamara
Tim McNamara

Posted on • Originally published at tim.mcnamara.nz

How type conversions work in Rust

Background

Rust is a strongly-typed programming language with static typing. It’s also quite pedantic. For example, it’s impossible to compare two integers with each other if they are of the different types. To avoid any extra work at runtime, such as reflection, Rust requires us to be precise with the types that our programs use.

This small program won't compile:

// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4a0ad78e651e74bc52d986037eb21b0c
fn main() {
    let a: i32 = 10;
    let b: u32 = 10;
    println!("{}", a + b);
}
Enter fullscreen mode Exit fullscreen mode

Instead creating a program that prints 20  to the console, we receive an error message with two error codes:

error[E0308]: mismatched types
 --> src/main.rs:4:22
  |
4 |   println!("{}", a + b); 
  |                      ^ expected `i32`, found `u32`

error[E0277]: cannot add `u32` to `i32`
 --> src/main.rs:4:20
  |
4 |   println!("{}", a + b); 
  |                    ^ no implementation for `i32 + u32`
  |
  = help: the trait `Add<u32>` is not implemented for `i32`
Enter fullscreen mode Exit fullscreen mode

For the curious, here is some information about how to interpret those error codes. The first (E0308) is caused because the addition operator (+) expects the same type for both of its operands. The second error (E0277) is subtly different. It’s saying that the trait bounds (the interface) are not satisfied. Addition is provided by the std::ops::Add  trait and has been implicitly parameterized to i32 type by its left operand.

Explicitly changing types with the as keyword

The most common method you’ll encounter to convert one type to another is via the as  keyword. It’s particularly common when converting between usize (which may be 32 or 64 bits wide, depending on CPU architecture) and fixed-width integers, such as u32.

One option that's available to us for fixing the earlier example is to promote both values to the type i64, which can represent all possible values within the u32 and i32 types:

// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b55bd9b4447b9b01d4f98649ed54edf6
fn main() {
    let a: i32 = 10;
    let b: u32 = 10;
    println!("{}", a as i64 + b as i64);
}
Enter fullscreen mode Exit fullscreen mode

If the value cannot fit within the bounds of the type being converted to — a negative number cannot be represented by an unsigned integer type, for example — then the program will exhibit very strange behaviour (see the "Rustnomicon" reference at the bottom of the article for details). We'll work through other options for handling cases where type conversions might fail later in the series.

The as keyword asks Rust to do the minimal amount of work possible to treat a value of one type as a value of another. Very often, such as converting between integer types of equal width, the internal representation does not change. This efficiency comes at a cost.

As is only available for primitive types. When you require conversions of types that you’ve designed yourself, you need to use the std::convert::{From,Into} traits.

The Rust reference provides a good overview of the details in the type cast expression page. The Rustnomicon also has a detailed section about casting between data types.


Posts to come in this series. Please follow me to get notified when they are posted!

  • Using the From and Into traits
  • Using the From and Into traits
  • Hidden type conversion via the Deref trait
  • Trait objects
  • Downcasting

Thanks to @vorfeedcanal for comments on a previous version of this post.

Discussion (4)

Collapse
yjdoc2 profile image
YJDoc2

Hey, great article!
I had a couple of questions :

  • As described, when changing between values of equal width, it does not change ; what happens if we convert u8 to u16 or to u32? Assuming these are defined on stack, does it repush again with correct width no. Of bytes or it (somehow) store it in register of full width and consider the final width no. Of bytes from there?
  • When converting from signed to unsigned or so, would it always crash? I don't remember 100% but we can convert 255 to -1 when going u8 to i8 ?

Looking forward to the series, I found out about the book from your last post, and the topics covered seem very interesting 😄😄
Thanks :)

Collapse
timclicks profile image
Tim McNamara Author • Edited on

As described, when changing between values of equal width, it does not change ; what happens if we convert u8 to u16 or to u32? Assuming these are defined on stack, does it repush again with correct width no. Of bytes or it (somehow) store it in register of full width and consider the final width no. Of bytes from there?

I'm not sure about what happens at the machine code level. I could imagine that the optimizer might avoid promoting values to a wider type if it's not necessary, but I don't think that there would be a difference between the data type on the stack and the data type held in registers.

When converting from signed to unsigned or so, would it always crash? I don't remember 100% but we can convert 255 to -1 when going u8 to i8 ?

Actually, I was wrong about this. Programs don't crash, they just silently continue with the wrong value. I have updated the article.

Collapse
vorfeedcanal profile image
VorfeedCanal

doc.rust-lang.org/nomicon/casts.html

casting from a larger integer to a smaller integer (e.g. u32 -> u8) will truncate
casting from a smaller integer to a larger integer (e.g. u8 -> u32) will
zero-extend if the source is unsigned
sign-extend if the source is signed

Someone is wrong. Either nomicon or this article.

Collapse
timclicks profile image
Tim McNamara Author

I'm wrong. I'll make some changes to the post.