Zum Inhalt springen

Integer-Überlauf in Rust

Jeder Ganzzahltyp in Rust hat eine feste Größe. Ein u8 hat Platz für die Zahlen von 0 bis 255 und für nichts sonst. Was passiert also, wenn du 255 hast und 1 dazuzählst? Das Ergebnis passt nicht hinein. Das nennt man Integer Overflow (Ganzzahlüberlauf).

Denk an den Kilometerzähler eines alten Autos mit nur drei Ziffern. Nach 999 Kilometern springt er zurück auf 000. Für ein Spielzeug ist das in Ordnung, für einen Kontostand ist es fatal. Rust hilft dir hier auf zwei Arten. Es meldet das Problem laut, während du entwickelst. Und es gibt dir einfache Methoden, mit denen du genau festlegst, was stattdessen passieren soll. Keine Sorge, es sind nur vier, und alle folgen demselben Muster. Diese Seite baut auf Datentypen und Operatoren auf.

Jeder Typ hat einen kleinsten Wert (MIN) und einen größten Wert (MAX). Du kannst sie mit Konstanten wie u8::MAX oder i32::MIN ausgeben:

fn main() {
println!("u8: {} bis {}", u8::MIN, u8::MAX);
println!("i8: {} bis {}", i8::MIN, i8::MAX);
println!("u16: {} bis {}", u16::MIN, u16::MAX);
println!("i32: {} bis {}", i32::MIN, i32::MAX);
println!("u32: {} bis {}", u32::MIN, u32::MAX);
println!("i64: {} bis {}", i64::MIN, i64::MAX);
println!("u64: {} bis {}", u64::MIN, u64::MAX);
println!("u128: {} bis {}", u128::MIN, u128::MAX);
}

Ausgabe:

u8: 0 bis 255
i8: -128 bis 127
u16: 0 bis 65535
i32: -2147483648 bis 2147483647
u32: 0 bis 4294967295
i64: -9223372036854775808 bis 9223372036854775807
u64: 0 bis 18446744073709551615
u128: 0 bis 340282366920938463463374607431768211455
▶ Selbst ausprobieren

Hier ist die ganze Familie in einer Tabelle:

Typ Bits Kleinster Wert Größter Wert
i8 / u8 8 -128 / 0 127 / 255
i16 / u16 16 -32.768 / 0 32.767 / 65.535
i32 / u32 32 etwa -2,1 Milliarden / 0 etwa 2,1 Milliarden / 4,3 Milliarden
i64 / u64 64 etwa -9,2 × 10^18 / 0 etwa 9,2 × 10^18 / 1,8 × 10^19
i128 / u128 128 etwa -1,7 × 10^38 / 0 etwa 1,7 × 10^38 / 3,4 × 10^38
isize / usize 64 auf den meisten Computern wie i64 / 0 wie i64 / u64

Ein vorzeichenbehafteter Typ (i) verwendet die Hälfte seines Bereichs für negative Zahlen. Ein vorzeichenloser Typ (u) beginnt bei 0, deshalb ist sein größter Wert etwa doppelt so groß. Alles außerhalb dieser Bereiche ist ein Überlauf. Auch wer unter MIN fällt (etwa 0 - 1 bei einem u8), erzeugt einen. Das nennt man manchmal Unterlauf (Underflow), aber Rust behandelt beides gleich.

Startest du dein Programm mit cargo run (ein Debug-Build), prüft Rust jedes +, - und *. Passt das Ergebnis nicht, hält das Programm mit einer klaren Meldung an. Hier addiert ein Spiel Bonuspunkte zu einem Punktestand vom Typ u8:

fn add_points(score: u8, bonus: u8) -> u8 {
score + bonus
}
fn main() {
let total = add_points(250, 10);
println!("Summe: {total}");
}

Wenn du es ausführst, bekommst du:

thread 'main' (12345) panicked at src/main.rs:2:5:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ Selbst ausprobieren

Rust meldet hier auf Englisch „attempt to add with overflow“, also „Versuch einer Addition mit Überlauf“, und nennt dir Datei, Zeile und Spalte. Die Werte laufen absichtlich über Funktionsparameter. Würdest du die Zahlen direkt neben das + schreiben, würde der Compiler das Problem schon vor dem Start entdecken (das siehst du gleich). Ein Absturz wirkt vielleicht hart, ist aber ein Gefallen: Du findest den Fehler auf deinem eigenen Rechner, mit der genauen Zeilennummer, statt in der Produktion einen falschen Punktestand zu bekommen. Mehr über Panics erfährst du auf der Seite Panic.

Baust du mit cargo run --release, sind diese Prüfungen abgeschaltet, damit der Code ein bisschen schneller läuft. Die Zahl läuft über (Wrap-around) wie der Kilometerzähler: 250 + 10 wird bei einem u8 zu 4 (denn 260 - 256 = 4).

Terminal window
cargo run --release
Summe: 4

Keine Panic, keine Warnung, nur eine stillschweigend falsche Zahl. Deshalb solltest du dich nie darauf verlassen, dass ein Überlauf „zufällig“ passiert. Willst du, dass die Zahl überläuft, sag es ausdrücklich mit wrapping_add (siehe unten). Die „Try it“-Buttons auf dieser Seite führen Debug-Builds aus. Mit wrapping_add kannst du dieses Verhalten also auch dort sehen:

fn main() {
let score: u8 = 250;
println!("{}", score.wrapping_add(10));
}

Ausgabe:

4
▶ Selbst ausprobieren

Sollen auch Release-Builds eine Panic auslösen, kannst du die Prüfungen in der Cargo.toml wieder einschalten:

[profile.release]
overflow-checks = true

Mit Zahlen direkt im Code kompiliert es gar nicht erst

Abschnitt betitelt „Mit Zahlen direkt im Code kompiliert es gar nicht erst“

Sieht der Compiler die ganze Rechnung, weigert er sich, sie zu bauen:

fn main() {
let level: u8 = 255 + 1;
println!("{level}");
}
error: this arithmetic operation will overflow
--> src/main.rs:2:21
|
2 | let level: u8 = 255 + 1;
| ^^^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow
|
= note: `#[deny(arithmetic_overflow)]` on by default

Der Compiler sagt: „Diese Rechenoperation wird überlaufen.“ Dasselbe gilt für ein Literal, das einfach zu groß ist, etwa let level: u8 = 256;. Das ergibt den Fehler literal out of range for u8 (Literal außerhalb des Bereichs von u8).

Statt zu hoffen, dass nie ein Überlauf passiert, kannst du festlegen, was dann passieren soll. Jeder Ganzzahltyp hat dafür vier Familien von Methoden. Sie funktionieren bei u8, i32, u64 und allen anderen gleich.

checked_: „Sag mir, wenn es nicht gepasst hat“

Abschnitt betitelt „checked_: „Sag mir, wenn es nicht gepasst hat““

checked_add, checked_sub und checked_mul geben ein Option zurück: Some(ergebnis), wenn es passt, und None, wenn nicht. Das ist die sicherste Wahl, wann immer eine falsche Zahl ein echtes Problem wäre, zum Beispiel bei Geld. Ein Kontostand kann nicht unter 0 fallen. Ein u64 mit checked_sub lehnt die Abhebung also für dich ab:

fn withdraw(balance: u64, amount: u64) -> Option<u64> {
balance.checked_sub(amount)
}
fn order_total(price: u32, quantity: u32, shipping: u32) -> Option<u32> {
price.checked_mul(quantity)?.checked_add(shipping)
}
fn main() {
let balance: u64 = 100;
for amount in [30, 250] {
match withdraw(balance, amount) {
Some(left) => println!("{amount} abgehoben, Kontostand jetzt {left}"),
None => println!("{amount} kann nicht abgehoben werden: nicht genug Geld"),
}
}
println!("{:?}", order_total(20, 3, 5));
println!("{:?}", order_total(2_000_000_000, 3, 5));
}

Ausgabe:

30 abgehoben, Kontostand jetzt 70
250 kann nicht abgehoben werden: nicht genug Geld
Some(65)
None
▶ Selbst ausprobieren

Weil das Ergebnis ein Option ist, funktioniert auch der ?-Operator. Läuft in order_total die Multiplikation über, gibt die Funktion sofort None zurück.

saturating_add und saturating_sub gehen nie über MIN oder MAX hinaus. Sie bleiben einfach dort stehen, wie ein volles Glas. Das ist perfekt für Dinge mit einer natürlichen Unter- oder Obergrenze: einen Lebensbalken, einen Lautstärkeregler, Artikel auf Lager.

fn main() {
let health: u8 = 20;
let after_hit = health.saturating_sub(35);
println!("Lebenspunkte nach einem harten Treffer: {after_hit}");
let volume: u8 = 90;
let louder = volume.saturating_add(25).min(100);
println!("Lautstärke nach dem Aufdrehen: {louder}");
let stock: u32 = 3;
let left = stock.saturating_sub(5);
println!("Artikel noch auf Lager: {left}");
}

Ausgabe:

Lebenspunkte nach einem harten Treffer: 0
Lautstärke nach dem Aufdrehen: 100
Artikel noch auf Lager: 0
▶ Selbst ausprobieren

Die Zeile mit der Lautstärke braucht einen Schritt mehr. saturating_add bleibt nur an der Grenze des Typs selbst stehen, bei einem u8 also bei 255. Dein Regler endet aber bei 100. Deshalb begrenzt .min(100) den Wert auf deine eigene Grenze.

Manchmal ist das Überlaufen genau das, was du willst. Ein Ticketzähler, der 254, 255, 0, 1 zählt. Oder eine einfache Hash-Funktion, bei der die Zahlen ohnehin durcheinandergewirbelt werden sollen. wrapping_add und wrapping_mul machen das in jedem Build, ob Debug oder Release:

fn simple_hash(text: &str) -> u32 {
let mut hash: u32 = 5381;
for byte in text.bytes() {
hash = hash.wrapping_mul(33).wrapping_add(byte as u32);
}
hash
}
fn main() {
let mut ticket: u8 = 254;
for _ in 0..4 {
println!("Ticket {ticket}");
ticket = ticket.wrapping_add(1);
}
println!("Hash von \"hello\": {}", simple_hash("hello"));
println!("Hash von \"a much longer sentence\": {}", simple_hash("a much longer sentence"));
}

Ausgabe:

Ticket 254
Ticket 255
Ticket 0
Ticket 1
Hash von "hello": 261238937
Hash von "a much longer sentence": 3373490575
▶ Selbst ausprobieren

Die Texte, die gehasht werden, sind absichtlich auf Englisch geblieben: Der Hash wird aus den einzelnen Bytes berechnet, und andere Wörter (oder Umlaute, die in UTF-8 zwei Bytes belegen) ergäben andere Zahlen. Der lange Satz würde einen u32 mit normalem * und + gleich mehrfach überlaufen lassen. Mit den wrapping_-Methoden ist das kein Problem: Genau dafür ist der Hash gedacht.

overflowing_add gibt ein Tupel zurück: das übergelaufene Ergebnis und einen bool, der sagt, ob ein Überlauf passiert ist. Das ist praktisch, wenn du den übergelaufenen Wert willst, aber auch wissen möchtest, dass es passiert ist:

fn main() {
let (value, overflowed) = 250u8.overflowing_add(10);
println!("{value}, übergelaufen: {overflowed}");
let (value, overflowed) = 50u8.overflowing_add(10);
println!("{value}, übergelaufen: {overflowed}");
}

Ausgabe:

4, übergelaufen: true
60, übergelaufen: false
▶ Selbst ausprobieren

Diese Variante brauchst du seltener als die anderen. Wenn du dir nicht sicher bist, welche du nehmen sollst, ist checked_ eine sehr gute Standardwahl.

Eine Typumwandlung mit as löst nie eine Panic aus, nicht einmal in Debug-Builds. Passt der Wert nicht, wird er stillschweigend zurechtgestutzt. Das überrascht viele, also sei vorsichtig damit:

fn main() {
let big: i32 = 300;
let negative: i32 = -1;
println!("300 as u8 = {}", big as u8);
println!("-1 as u32 = {}", negative as u32);
match u8::try_from(big) {
Ok(small) => println!("passt: {small}"),
Err(e) => println!("300 passt nicht in einen u8: {e}"),
}
match u8::try_from(200) {
Ok(small) => println!("passt: {small}"),
Err(e) => println!("200 passt nicht in einen u8: {e}"),
}
}

Ausgabe:

300 as u8 = 44
-1 as u32 = 4294967295
300 passt nicht in einen u8: out of range integral type conversion attempted
passt: 200
▶ Selbst ausprobieren

Die Fehlermeldung „out of range integral type conversion attempted“ kommt von Rust selbst und bedeutet: Es wurde versucht, eine Ganzzahl in einen Typ umzuwandeln, dessen Bereich sie nicht abdeckt. 300 as u8 behält nur die untersten 8 Bits, und das ergibt 44. -1 as u32 wird zum größten u32. Keine der beiden Umwandlungen warnt dich. u8::try_from prüft dagegen zuerst und gibt dir ein Result, sodass du entscheiden kannst, was passieren soll. Eine gute Gewohnheit: Nimm as nur, wenn du weißt, dass der Wert passt, und try_from, wenn er von außerhalb deines Programms kommt. (Kommazahlen verhalten sich anders: 300.7 as u8 bleibt bei 255 stehen, statt überzulaufen.) Die Seiten Typumwandlung und Konvertierungen gehen tiefer darauf ein.

Noch ein paar Dinge, über die viele stolpern, alle an einer Stelle:

fn main() {
println!("7 / 2 = {}", 7 / 2);
println!("-7 / 2 = {}", -7 / 2);
println!("i32::MIN.checked_abs() = {:?}", i32::MIN.checked_abs());
println!("i32::MIN.unsigned_abs() = {}", i32::MIN.unsigned_abs());
println!("10u32.checked_pow(9) = {:?}", 10u32.checked_pow(9));
println!("10u32.checked_pow(10) = {:?}", 10u32.checked_pow(10));
}

Ausgabe:

7 / 2 = 3
-7 / 2 = -3
i32::MIN.checked_abs() = None
i32::MIN.unsigned_abs() = 2147483648
10u32.checked_pow(9) = Some(1000000000)
10u32.checked_pow(10) = None
▶ Selbst ausprobieren
  • Ganzzahldivision lässt den Rest weg. Sie rundet nicht, sondern schneidet in Richtung null ab. Deshalb ist -7 / 2 gleich -3. Für den Rest nimmst du %, für Nachkommastellen Kommazahlen.
  • abs() kann überlaufen. Ein i32 reicht hinunter bis -2147483648, aber nur hinauf bis 2147483647. Die positive Version von i32::MIN passt also nicht hinein. i32::MIN.abs() löst in Debug-Builds eine Panic mit attempt to negate with overflow aus (Versuch einer Negation mit Überlauf). Nimm checked_abs() oder unsigned_abs(). Letzteres gibt einen u32 zurück, in den der Wert passt.
  • pow wächst schnell. 10 hoch 10 ist schon zu groß für einen u32. Es gibt auch checked_pow, saturating_pow und wrapping_pow.

Soll ein Wert immer überlaufen oder immer am Rand stehen bleiben, kannst du ihn in std::num::Wrapping oder std::num::Saturating verpacken. Dann machen ganz normale + und - das Richtige, und du kannst es nicht vergessen:

use std::num::{Saturating, Wrapping};
fn main() {
let mut ticket = Wrapping(254u8);
ticket += 1;
ticket += 1;
ticket += 1;
println!("Ticket: {ticket}");
let mut battery = Saturating(10u8);
battery -= 25;
println!("Akku: {battery}");
}

Ausgabe:

Ticket: 1
Akku: 0
▶ Selbst ausprobieren
Methodenfamilie Gibt zurück Nimm sie, wenn
+ - * Die Zahl (Panic im Debug-Build, Überlauf im Release-Build) du sicher bist, dass nichts überlaufen kann
checked_add und Verwandte Option<T> eine falsche Zahl ein Fehler wäre: Geld, Größen, IDs
saturating_add und Verwandte Die Zahl, festgehalten bei MIN oder MAX es eine natürliche Unter- oder Obergrenze gibt: Lebenspunkte, Lautstärke, Lagerbestand
wrapping_add und Verwandte Die Zahl, übergelaufen das Überlaufen gewollt ist: Hashes, Zähler, die im Kreis laufen
overflowing_add und Verwandte (T, bool) du den übergelaufenen Wert willst und wissen möchtest, ob es passiert ist
T::try_from(x) Result<T, _> du einen Wert umwandelst, der vielleicht nicht in einen kleineren Typ passt

len() gibt einen usize zurück, und der kann nicht negativ sein. Bei einer leeren Liste wäre len() - 1 gleich -1, also läuft es über:

fn last_index(tasks: &[String]) -> usize {
tasks.len() - 1
}
fn main() {
let tasks: Vec<String> = Vec::new();
println!("die letzte Aufgabe steht an Index {}", last_index(&tasks));
}

Wenn du es ausführst, bekommst du:

thread 'main' (12345) panicked at src/main.rs:2:5:
attempt to subtract with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
▶ Selbst ausprobieren

„attempt to subtract with overflow“ heißt: Versuch einer Subtraktion mit Überlauf. Darauf fällt fast jeder mindestens einmal herein. Die Lösung: Lass den Typ ausdrücken, dass die Liste leer sein könnte. Gib tasks.len().checked_sub(1) zurück, das ist ein Option<usize>. Willst du nur das letzte Element, ist tasks.last() sogar noch einfacher.

Drei Testergebnisse von 90, 85 und 100 passen jeweils in einen u8. Ihre Summe, 275, passt nicht. Deshalb löst let total: u8 = scores.iter().sum(); eine Panic mit attempt to add with overflow aus. Die Panic zeigt auf eine Stelle in der Standardbibliothek, was verwirren kann. Die Ursache ist aber dein u8. Wandle jeden Wert vor dem Addieren in einen größeren Typ um:

fn main() {
let scores: Vec<u8> = vec![90, 85, 100];
let total: u32 = scores.iter().map(|&s| s as u32).sum();
println!("Summe: {total}, Durchschnitt: {}", total / 3);
}

Ausgabe:

Summe: 275, Durchschnitt: 91
▶ Selbst ausprobieren

Von u8 nach u32 mit as umzuwandeln ist immer sicher, denn jeder u8 passt in einen u32.

Der Akkustand eines Handys ist ein u8 von 0 bis 100. Schreib charge(level: u8, amount: u8) -> u8 und drain(level: u8, amount: u8) -> u8, die nie über 100 steigen und nie unter 0 fallen. Starte bei 80, lade um 30 auf, entlade dann um 130 und gib nach jedem Schritt den Stand aus. Tipp: saturating_add plus .min(100) zum Laden, saturating_sub zum Entladen.

Lösung anzeigen
fn charge(level: u8, amount: u8) -> u8 {
level.saturating_add(amount).min(100)
}
fn drain(level: u8, amount: u8) -> u8 {
level.saturating_sub(amount)
}
fn main() {
let mut level: u8 = 80;
println!("Start: {level}%");
level = charge(level, 30);
println!("nach dem Laden: {level}%");
level = drain(level, 130);
println!("nach einem langen Tag: {level}%");
}

Ausgabe:

Start: 80%
nach dem Laden: 100%
nach einem langen Tag: 0%
▶ Selbst ausprobieren

saturating_sub bleibt von selbst bei 0 stehen. Beim Laden liegt die Grenze des Typs bei 255, deshalb setzt .min(100) deine eigene Grenze obendrauf.

Schreib fn factorial(n: u64) -> Option<u64>, die 1 × 2 × … × n multipliziert und None zurückgibt, wenn das Ergebnis zu groß für einen u64 wird. Gib das Ergebnis für 5, 20 und 21 aus. Tipp: Fang mit let mut result: u64 = 1; an und nimm in einer for-Schleife result.checked_mul(i)?.

Lösung anzeigen
fn factorial(n: u64) -> Option<u64> {
let mut result: u64 = 1;
for i in 1..=n {
result = result.checked_mul(i)?;
}
Some(result)
}
fn main() {
for n in [5, 20, 21] {
match factorial(n) {
Some(value) => println!("{n}! = {value}"),
None => println!("{n}! ist zu groß für einen u64"),
}
}
}

Ausgabe:

5! = 120
20! = 2432902008176640000
21! ist zu groß für einen u64
▶ Selbst ausprobieren

Das ? gibt None aus der ganzen Funktion zurück, sobald eine einzige Multiplikation nicht passt. 20! ist die größte Fakultät, die ein u64 aufnehmen kann.

  • Jeder Ganzzahltyp hat einen festen Bereich, von T::MIN bis T::MAX. Wer ihn verlässt, erzeugt einen Überlauf.
  • Debug-Builds lösen eine Panic mit attempt to add with overflow aus. Release-Builds laufen stillschweigend über. Einen Überlauf, den der Compiler sehen kann, meldet er als Compilerfehler.
  • Wähl das Verhalten selbst: checked_ liefert ein Option, saturating_ bleibt am Rand stehen, wrapping_ läuft über, overflowing_ liefert den Wert und einen bool.
  • as prüft nie und kann eine Zahl stillschweigend verändern. Nimm T::try_from, wenn ein Wert vielleicht nicht passt.
  • Vorsicht bei len() - 1 auf leeren Listen, bei Summen in kleinen Typen, bei abs() von MIN und bei pow.

Kurzer Check

1. Was passiert mit 250u8 + 10 in einem Debug-Build, wenn die Werte aus Variablen kommen?

2. Was gibt 100u64.checked_sub(250) zurück?

3. Ein Lebensbalken soll nie unter 0 fallen. Welche Methode passt am besten?

4. Was ergibt 300_i32 as u8?

Punkte: 0 / 4