Rust-lernen.de

Ganzzahl-Überlauf in Rust vs. Java

Letzte Änderung: 06.05.2024

Ein normalerweise weniger beachtetes Thema: Was passiert, wenn bei Rechenoperationen eine Zahl größer wird als vorgesehen? Der nachfolgende Artikel beleuchtet diese Frage im Detail.

Ganzzahl-Überlauf

Standardverhalten in Rust

Sämtliche Standard-Datentypen in Rust (und den meisten anderen Programmiersprachen) haben eine feste Speichergröße in Bits und damit auch einen begrenzen Wertebereich. Beispielsweise hat der Datentyp u8 einen Wertebereich von 0 bis 255. Es gibt sogar Konstanten für den kleinsten und größten Wert im Werbereich einer Zahl, z.B. u8::MAX = 255.

Was passiert nun, wenn eine Zahl zu groß für den Wertebereich ist? Hier ein Beispiel:

fn main() {
    let a: u8 = 254;
    let b: u8 = 3;
    let summe = a + b;
    println!("{summe}");
}

Schon beim Versuch, obiges Programm zu kompilieren, bricht der Compiler mit einer Fehlermeldung ab und weist auf den drohenden Überlauf hin:

let summe = a + b;
            ^^^^^ attempt to compute `254_u8 + 3_u8`, which would overflow

Ok, aber was passiert in einem komplexeren Fall, bei dem der Compiler den Überlauf nicht vorhersehen kann? Um das zu simulieren, brauchen wir den Compiler nicht auszutricksen, sondern können ihm das direkt über die Annotation #[allow(arithmetic_overflow)] mitteilen:

#[allow(arithmetic_overflow)]
fn main() {
    let a: u8 = 254;
    let b: u8 = 3;
    let summe = a + b;
    println!("{summe}");
}

Nun kompiliert der Code und wir können ihn ausführen. Doch als Konsolenausgabe erhalten wir eine Fehlermeldung:

thread 'main' panicked at main.rs:5:17:
attempt to add with overflow

Rust prüft also auch zur Laufzeit, ob es einen Überlauf gab und bricht das Programm ggf. ab. Dies ist allerdings nur das Standardverhalten im Debug-Modus. Kompiliert man das Programm hingegen mit dem Parameter --release, bekommen wir folgende Ausgabe:

1

Das Programm bricht nun nicht mehr ab, sondern ignoriert den Überlauf. Der Überlauf verhält sich so, als ob auf die Zahl 254 die Zahlen 255, 0, 1, 2, usw. folgen. Man bezeichnet dies als Zweierkomplement-Umbruch (engl. two’s complement wrap). Analog kann es auch bei einer Multiplikation und Subtraktion zu einem Überlauf kommen (nicht zu verwechseln mit dem sog. Unterlauf in der Nähe von null bei Gleitkommazahlen).

Analog verhält es sich bei vorzeichenbehafteten Zahlen. Sehen wir uns ein Beispiel mit dem Datentyp i8 an:

#[allow(arithmetic_overflow)]
fn main() {
    let a: i8 = 126;
    let b: i8 = 3;
    let summe = a + b;
    println!("{summe}");
}

Lassen wir das Programm wieder mit dem Parameter --release laufen, erhalten wir als Ausgabe:

-127

Der Überlauf verhält sich so, als ob auf die Zahl 126 die Zahlen 127, -128, -127, -126, usw. folgen.

Standardverhalten in Java

Wie verhält es sich bei Java? Testen wir das mit folgendem Programm:

public class GanzzahlÜberlauf {
    public static void main(String[] args) {
        int a = Integer.MAX_VALUE;
        int b = 3;
        int summe = a + b;
        System.out.println(summe);
    }
}

Bei Java gibt es keinerlei Warnungen, das Programm läuft durch und gibt folgendes aus:

-2147483646

Übrigens: In Java werden die Datentypen byte und short von der JVM automatisch in int umgewandelt, daher funktioniert obiges Programmbeispiel nicht mit byte oder short.

Erkennen eines Überlaufs

Wie kann man nun erkennen, ob es einen Überlauf gab?

Technisch gesehen hat jede CPU ein sog. Überlaufbit in einem speziellen Register, das nach jeder Rechenoperation ausgelesen werden kann.

Rust stellt eine Reihe von Methoden zur Verfügung, die einen Überlauf erkennen: checked_* und overflowing_*. Sehen wir uns ein Beispiel mit checked_add() an:

fn main() {
    let a: u8 = 254;
    let b: u8 = 3;
    let summe = a.checked_add(b);
    if let Some(ergebnis) = summe {
        println!("{ergebnis}");
    } else {
        println!("Überlauf!")
    }
}

checked_add() gibt einen Wert vom Typ Option<u8> zurück, der im Falle eines Überlaufs None ist. Obiges Programm erzeugt folgende Ausgabe:

Überlauf!

Will man den Ergebniswert inkl. Überlauf und das Überlaufbit gleichzeitig erhalten, kann man die Methode overflowing_add() verwenden:

fn main() {
    let a: u8 = 254;
    let b: u8 = 3;
    let summe = a.overflowing_add(b);
    println!("Ergebnis: {}, Überlauf: {}", summe.0, summe.1);
}

Die Ausgabe ist in diesem Fall:

Ergebnis: 1, Überlauf: true

In Java gibt es eine ähnliche Lösung z.B. mit der Methode Math.addExact(), die im Falle eines Überlaufs eine Exception wirft. Hier ein Beispiel:

public class ÜberlaufErkennen {
    public static void main(String[] args) {
        int a = Integer.MAX_VALUE;
        int b = 3;
        try {
            int summe = Math.addExact(a, b);
            System.out.println(summe);
        }
        catch (ArithmeticException e) {
            System.out.println(e.toString());
        }
    }
}

Als Ausgabe erhalten wir:

java.lang.ArithmeticException: integer overflow

Vermeiden eines Überlaufs

Macht ein Überlauf keinen Sinn, kann man diesen mit den Methoden saturating_* vermeiden und das Ergebnis auf den minimal bzw. maximal möglichen Wert des Wertebereichs limitieren (quasi runden):

fn main() {
    let a: u8 = 254;
    let b: u8 = 3;
    let summe = a.saturating_add(b);
    println!("{summe}");
}

Obiges Beispiel hat folgende Ausgabe:

255

Überlauf ohne Fehlerbehandlung

Will man einen Überlauf explizit in Kauf nehmen und auch im Debug-Modus von Rust keine Fehlermeldung bekommen, verwendet man die Methoden wrapping_*. Hier ein Beispiel:

fn main() {
    let a: u8 = 254;
    let b: u8 = 3;
    let summe = a.wrapping_add(b);
    println!("{summe}");
}

Quellen