Numeric Overflow

Let's take a very simple C program:

#include <stdio.h>

int main() {
   char a = 127;
   a = a + 1;
   printf("a = %d\n", a);
   return 0;
}

If you haven't been programming for a while, you may be surprised that the output is -127. Your 8-bit signed integer (char) can only hold -128 through 127, in binary two's complement. A binary addition of 1 to 127 gives 10000000. Since the first digit in two's complemenet binary represents the sign bit---you get -127.

Rust's behavior for this program varies by how you compiled your program. In default debug mode:

fn main() {
    let mut a: i8 = 127;
    a += 1;
    println!("{a}");
}

Crashes the program with attempt to add with overflow. (If you compile in release mode with cargo run --release), it prints the wrapped number.

Always test your builds in `debug`` mode!

Explicitly Handling Wrapping

In C, you can detect this overflow with some additional code:

#include <stdio.h>
#include <limits.h>

int main() {
   char a = 127;
   char add = 1;
   if (a > 0 && add > 0 && a > CHAR_MAX - add) {
      printf("Overflow detected\n");
      return 1;
   }
   a = a + add;
   printf("a = %d\n", a);
   return 0;
}

(You may also want to check for underflow)

Rust includes checked_ arithmetic for this purpose:

fn main() {
    let a: i8 = 127;
    let a = a.checked_add(1);
    println!("{a:?}");
}

This prints None. That's odd! checked_add returns an Option type, which is fundamentally Rust's alternative to null/nullptr. Just like a Result, an Option is a sum type that can either be None, or Some(x).

Notice that I snuck in :? in the print. This is "debug printing", and prints the contents of complicated types if they implement the appropriate trait.

You can also unwrap options:

fn main() {
    let a: i8 = 127;
    let a = a.checked_add(1).unwrap();
    println!("{a}");
}

But I Want to Wrap!

Sometimes, wrapping is the desired behavior. It's used a lot in cryptographic functions, for example. Rust lets you opt in to the wrapping behavior:

fn main() {
    let a: i8 = 127;
    let a = a.wrapping_add(1);
    println!("{a}");
}

This won't crash on debug or release builds: you've explicitly told Rust (and whomever reads your code later) that wrapping was the intended behavior, and not a bug.

Saturating

Maybe you'd rather saturate at the maximum possible value?

fn main() {
    let a: i8 = 127;
    let a = a.saturating_add(1);
    println!("{a}");
}

This prints 127.

Other Operations

Checked, saturating and wrapping variants of addition, subtraction, multiplication and division are all provided (division checking checks for divide by zero).

If you are sensing a theme, it's that Rust picks safe by default when possible---and gives you the chance to opt out. C and C++ tend to assume you know what you're doing, and offer the option of adding safety checks.