On integer types in Rust

Rust programming language differentiates between signed and unsigned integer types. In this article I will show the implications of such approach.

You have probably noticed, that Rust comes with multiple integer types: u32 , i8 , usize , isize — to name a few. Most probably, you also noticed, that they all have something in common — they are prefixed with either u (standing for unsigned) or i (standing for, well… integer).

Most modern languages don’t have this distinction — while for example Java has byte , short , int and long types for growing ranges of minimum and maximum values, they can all store both negative and positive integers, and no built-in types exist for keeping only positive ones.

This distinction is nowadays mostly specific to systems programming languages, like C++, Go and Rust.

Unsigned vs signed

Unsigned integer types are those, that cannot hold negative values. Simply put — they cannot go below zero (think of unsigned as those, that can’t have a minus sign in front), and zero is exactly what is returned by their min_value() method.

There are 5 “stable” unsigned integer types in Rust: u8 , u16 , u32 , u64 and usize . Additionally, u128 exists in nightly-only experimental branch of the language.

Signed, on the other hand, are able to store both negative and positive values. There are, as well, 5 “stable” signed integer types: i8 , i16 , i32 , i64 and isize , plus nightly-only i128 as a sixth type.

8, 16, 32, 64…

Now, what do the numbers (like in u8 or i32 ) mean?

These numbers define amount of bits used to store the number. The bigger number is, the bigger maximum value can be stored within variable of given type.

Given, that u8 uses 8 bits, it can store a total of 2⁸=256 unique integer values. For u8 (unsigned type), this means that maximum integer value that can be stored is 255 (why not 256? don’t forget about the zero — the range of possible values is 0–255).

On the other hand, u8 ’s signed equivalent, i8 can also store 256 unique integer values. However, it must be able to store an equal amount of unique negative and positive values! Hence, the range of possible values is -128 through 127 (again, zero is considered a positive value).

There are few conclusions about calculating values that can be stored with signed and unsigned integers:

the range of possible values is calculated with an equation: range = 2^X where X is number of bits (8, 16, 32, 64 and so on);

where is number of bits (8, 16, 32, 64 and so on); the maximum value for unsigned type is max_u = range — 1 ; the minimum value for unsigned type is always min_u = 0 ;

type is ; the minimum value for type is always ; the maximum value for signed type is max_i = (max_u — 1) / 2 ; the minimum value for signed type is min_i = -range / 2 ;

usize and isize

There are two special cases — usize and isize . As you might have noticed, they also come with either u or i prefix, but don’t specify the bit size directly.

usize and isize are pointer-sized. Don’t worry about the accurate meaning of this — truth is, you only need to know, that their bit size depends on the hardware’s architecture. On 32-bit systems, usize is equivalent of u32 . For 64-bits… you guessed it — u64 .

Is that distinction useful?

Certainly! It is definitely helpful to use unsigned types where negative values don’t make sense:

it wouldn’t make sense to create a ThreadPool with negative number of threads, would it?

That way, another aspect of type safety is guaranteed. Attempting to assign negative value to unsigned type variable results in compiler error!

Plus, unsigned types can store twice as big maximum value than their signed equivalents. This is especially useful in memory-critical environments (like embedded). Why allocate too much memory for i64 , while all you might have needed was u32 for storing some large, positive number?

Cool. Is that all?

No! There are few interesting things about the behaviour implied by these integer types.

Attempting to assign negative integer value to unsigned type variable

If you try to assign a negative value to a variable declared as unsigned type, you’ll get a compiler error — that’s rather obvious, and we saw that in ThreadPool example — however, let’s take a look at the compiler’s error message:

The compiler is smart and it knows that it is illegal to put a minus sign in front of unsigned type.

That way, the most trivial errors are prevented in compile time.

Comparison warnings

Now this is the feature I especially love — the compiler warned me, that my comparison does not make sense!

comparison is useless due to type limits — isn’t that beautiful? The if statement’s condition will never be fulfilled, because unsigned variable b will never hold a value lesser than zero. It’s not an error, however, to have statements that don’t make sense, so it is only compiler warning.

Attempt to subtract, add or multiply with overflow

If, in the runtime, happens that an operation on two integers (resulting in third one — be it subtraction, addition or multiplication) produces a value that does not fit within the range of given integer type, a panic occurs.

5–6 = -1. Sorry, can’t store that in unsigned integer type. I quit!

Now one could argue — is runtime panic really the best option? Well, I personally think that it is. The program detected a runtime problem, assumed that this is probably not what we intended to do and stopped the execution. My understanding is that this is much better solution than what would happen, for example, in C++ code:

Every kid knows, that 5 minus 6 gives 4294967295. Or does it?

An integer overflow occurs — the code above “flips the counter” and prints the maximum value of C++’s 32-bit unsigned integer type uint . There is no runtime error — the program happily continued — and, in my opinion, such behaviour can have disastrous consequences, because it continued the execution with totally messed up value.

Again, I think Rust does it right.

Literal out of range warning

While we saw above, that Rust halts the execution in runtime to prevent integer overflows, it does so mainly because there will be cases, where at least one of the values cannot be known at compile time (for example it is read as user’s input).

On the other hand, let’s see what happens if we try to assign a value larger than the maximum value for given type up front:

compiler says: “what you do is stupid, but I warned you.”

Despite obvious integer overflow, this code compiles without errors and prints 44 *. But, it gives us a warning, saying “hey, you are probably doing something wrong, don’t blame me afterwards — I told you!”.

*If you are wondering, why 44 : it takes a value of 300 in line 2 and subtracts 2⁸ (256, total “amount” of unique values that can be stored in u8 type) from it, resulting in, well — 44 .

Summary

While using integers in Rust, you have to ask yourself if you need the variable to store both negative and positive values, or positive only. For memory optimisations, it is also advised to wisely choose the “size” of the variable to use. Fortunately, Rust’s great compiler has got you covered in most scenarios where things could go south.

For more useful informations — take a look at the documentation. Integer types also come with many handy methods, like testing if overflow would occur on add. As always, The Book also provides a nice, concise section about integer types in Rust.

I strongly recommend to check my article about why I stepped into Rust!