Salta ai contenuti

Overflow degli interi in Rust

Ogni tipo intero in Rust ha una dimensione fissa. Un u8 ha spazio per i numeri da 0 a 255, e nient’altro. Quindi cosa succede se hai 255 e aggiungi 1? Il risultato non ci sta. Si chiama overflow degli interi (traboccamento).

Pensa al contachilometri di una vecchia auto con solo tre cifre. Dopo 999 chilometri torna a 000. Per un giocattolo va bene, ma per il saldo di un conto in banca è un disastro. Rust ti aiuta in due modi. Segnala il problema a gran voce mentre sviluppi. E ti dà metodi semplici per dire esattamente cosa deve succedere invece. Niente paura: sono solo quattro, e seguono tutti lo stesso schema. Questa pagina parte da Tipi di dati e Operatori.

Ogni tipo ha un valore minimo (MIN) e un valore massimo (MAX). Puoi stamparli con costanti come u8::MAX o i32::MIN:

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

Output:

u8: da 0 a 255
i8: da -128 a 127
u16: da 0 a 65535
i32: da -2147483648 a 2147483647
u32: da 0 a 4294967295
i64: da -9223372036854775808 a 9223372036854775807
u64: da 0 a 18446744073709551615
u128: da 0 a 340282366920938463463374607431768211455
▶ Provalo tu

Ecco tutta la famiglia in una tabella:

Tipo Bit Minimo Massimo
i8 / u8 8 -128 / 0 127 / 255
i16 / u16 16 -32.768 / 0 32.767 / 65.535
i32 / u32 32 circa -2,1 miliardi / 0 circa 2,1 miliardi / 4,3 miliardi
i64 / u64 64 circa -9,2 × 10^18 / 0 circa 9,2 × 10^18 / 1,8 × 10^19
i128 / u128 128 circa -1,7 × 10^38 / 0 circa 1,7 × 10^38 / 3,4 × 10^38
isize / usize 64 sulla maggior parte dei computer come i64 / 0 come i64 / u64

Un tipo con segno (i) usa metà del suo intervallo per i numeri negativi. Un tipo senza segno (u) parte da 0, quindi il suo valore massimo è circa il doppio. Tutto quello che sta fuori da questi intervalli è un overflow, e conta anche scendere sotto MIN (come 0 - 1 su un u8). Quest’ultimo a volte si chiama underflow, ma Rust lo tratta allo stesso modo.

Quando esegui il programma con cargo run (una build di debug), Rust controlla ogni +, - e *. Se il risultato non ci sta, il programma si ferma con un messaggio chiaro. Qui un gioco aggiunge punti bonus a un punteggio u8:

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

Quando lo esegui, ottieni:

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
▶ Provalo tu

Il messaggio (in inglese, come tutti quelli di Rust) dice che il thread main è andato in panic alla riga 2 per un «tentativo di somma con overflow». I valori passano apposta dai parametri di una funzione. Se scrivessi i numeri proprio accanto al +, il compilatore vedrebbe il problema prima ancora che il programma parta (lo vedrai tra poco). Un crash può sembrare brutale, ma è una gentilezza: trovi il bug sul tuo computer, con il numero di riga esatto, invece di ritrovarti un punteggio sbagliato in produzione. Trovi di più sui panic nella pagina Panic.

Quando compili con cargo run --release, quei controlli vengono disattivati per rendere il codice un po’ più veloce. Il numero fa il giro (wrap around) come il contachilometri: 250 + 10 su un u8 diventa 4 (perché 260 - 256 = 4).

Terminal window
cargo run --release
totale: 4

Niente panic, niente avvisi: solo un numero sbagliato in silenzio. Ecco perché non dovresti contare su un overflow che succede «per caso». Se vuoi che il numero faccia il giro, dillo con wrapping_add (lo vedi più sotto). I pulsanti «Provalo tu» di questo sito eseguono build di debug, quindi wrapping_add è anche il modo per vedere lì questo comportamento:

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

Output:

4
▶ Provalo tu

Se vuoi che anche le build di release vadano in panic, puoi riattivare i controlli in Cargo.toml:

[profile.release]
overflow-checks = true

Con numeri scritti direttamente non compila nemmeno

Sezione intitolata “Con numeri scritti direttamente non compila nemmeno”

Quando il compilatore vede tutto il calcolo, si rifiuta di compilarlo:

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

Il compilatore avvisa che questa operazione aritmetica andrà in overflow: u8::MAX + 1 non ci sta. Lo stesso vale per un letterale semplicemente troppo grande, come let level: u8 = 256;, che dà literal out of range for u8 (letterale fuori dall’intervallo di u8).

Invece di sperare che l’overflow non succeda mai, puoi scegliere cosa deve succedere. Ogni tipo intero ha quattro famiglie di metodi per questo. Funzionano tutte allo stesso modo su u8, i32, u64 e gli altri.

checked_add, checked_sub e checked_mul restituiscono un Option: Some(risultato) se ci sta, None se non ci sta. È la scelta più sicura ogni volta che un numero sbagliato sarebbe un problema serio, come con i soldi. Il saldo di un conto non può scendere sotto 0, quindi un u64 con checked_sub rifiuta il prelievo al posto tuo:

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!("Prelevati {amount}, ora il saldo è {left}"),
None => println!("Impossibile prelevare {amount}: soldi insufficienti"),
}
}
println!("{:?}", order_total(20, 3, 5));
println!("{:?}", order_total(2_000_000_000, 3, 5));
}

Output:

Prelevati 30, ora il saldo è 70
Impossibile prelevare 250: soldi insufficienti
Some(65)
None
▶ Provalo tu

Siccome il risultato è un Option, funziona anche l’operatore ?. In order_total, se la moltiplicazione va in overflow, la funzione restituisce subito None.

saturating_add e saturating_sub non vanno mai oltre MIN o MAX. Si fermano lì, come un bicchiere pieno. È perfetto per le cose che hanno un minimo o un massimo naturale: una barra della salute, una manopola del volume, gli articoli in magazzino.

fn main() {
let health: u8 = 20;
let after_hit = health.saturating_sub(35);
println!("salute dopo un colpo forte: {after_hit}");
let volume: u8 = 90;
let louder = volume.saturating_add(25).min(100);
println!("volume dopo averlo alzato: {louder}");
let stock: u32 = 3;
let left = stock.saturating_sub(5);
println!("articoli rimasti in magazzino: {left}");
}

Output:

salute dopo un colpo forte: 0
volume dopo averlo alzato: 100
articoli rimasti in magazzino: 0
▶ Provalo tu

La riga del volume ha bisogno di un passo in più. saturating_add si ferma solo al limite del tipo stesso, 255 per un u8. La tua manopola si ferma a 100, quindi .min(100) la blocca al tuo limite.

A volte fare il giro è proprio quello che vuoi. Un contatore di biglietti che va 254, 255, 0, 1. Oppure una semplice funzione di hash, dove i numeri devono rimescolarsi. wrapping_add e wrapping_mul fanno così in ogni build, di debug o di 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!("biglietto {ticket}");
ticket = ticket.wrapping_add(1);
}
println!("hash di \"hello\": {}", simple_hash("hello"));
println!("hash di \"a much longer sentence\": {}", simple_hash("a much longer sentence"));
}

Output:

biglietto 254
biglietto 255
biglietto 0
biglietto 1
hash di "hello": 261238937
hash di "a much longer sentence": 3373490575
▶ Provalo tu

I testi passati all’hash sono rimasti in inglese apposta: l’hash dipende da ogni singolo byte, quindi con parole diverse otterresti numeri diversi. La frase lunga, con i normali * e +, farebbe andare in overflow un u32 molte volte. Con i metodi wrapping_ va benissimo: è proprio quello che l’hash deve fare.

overflowing_add restituisce una tupla: il risultato dopo il giro e un bool che dice se c’è stato overflow. È comodo quando vuoi il valore dopo il giro ma vuoi anche sapere che è successo:

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

Output:

4, overflow: true
60, overflow: false
▶ Provalo tu

Userai questo meno spesso degli altri. Se non sai quale scegliere, checked_ è un’ottima scelta predefinita.

Convertire tra tipi con as non va mai in panic, nemmeno nelle build di debug. Se il valore non ci sta, viene tagliato in silenzio finché ci sta. Sorprende molte persone, quindi fai attenzione:

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!("ci sta: {small}"),
Err(e) => println!("300 non ci sta in un u8: {e}"),
}
match u8::try_from(200) {
Ok(small) => println!("ci sta: {small}"),
Err(e) => println!("200 non ci sta in un u8: {e}"),
}
}

Output:

300 as u8 = 44
-1 as u32 = 4294967295
300 non ci sta in un u8: out of range integral type conversion attempted
ci sta: 200
▶ Provalo tu

Il testo dell’errore («out of range integral type conversion attempted», cioè tentata una conversione intera fuori intervallo) viene dalla libreria standard ed è in inglese. 300 as u8 tiene solo gli 8 bit più bassi, cioè 44. -1 as u32 diventa il u32 più grande. Nessuno dei due ti avvisa. u8::try_from controlla prima e ti dà un Result, così puoi decidere cosa fare. Una buona abitudine: usa as solo quando sai che il valore ci sta, e try_from quando il valore arriva da fuori del tuo programma. (I float si comportano diversamente: 300.7 as u8 si ferma a 255 invece di fare il giro.) Le pagine Conversione di tipo e Conversioni vanno più a fondo.

Qualche altra cosa che fa inciampare, tutta in un posto:

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));
}

Output:

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
▶ Provalo tu
  • La divisione intera scarta il resto. Non arrotonda; taglia verso lo zero, quindi -7 / 2 fa -3. Usa % per il resto, o i float se vuoi i decimali.
  • abs() può andare in overflow. Un i32 scende fino a -2147483648 ma sale solo fino a 2147483647, quindi la versione positiva di i32::MIN non ci sta. i32::MIN.abs() va in panic nelle build di debug con attempt to negate with overflow. Usa checked_abs(), oppure unsigned_abs(), che restituisce un u32 in cui il valore ci sta.
  • pow cresce in fretta. 10 alla decima è già troppo grande per un u32. Esistono anche checked_pow, saturating_pow e wrapping_pow.

Se un valore deve sempre fare il giro o sempre saturare, puoi avvolgerlo in std::num::Wrapping o std::num::Saturating. Così i normali + e - fanno la cosa giusta, e non puoi dimenticartene:

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

Output:

biglietto: 1
batteria: 0
▶ Provalo tu
Famiglia di metodi Restituisce Usala quando
+ - * Il numero (panic in debug, fa il giro in release) Sei sicuro che non possa andare in overflow
checked_add e simili Option<T> Un numero sbagliato sarebbe un bug: soldi, dimensioni, ID
saturating_add e simili Il numero, bloccato a MIN o MAX C’è un minimo o un massimo naturale: salute, volume, magazzino
wrapping_add e simili Il numero, dopo il giro Fare il giro è lo scopo: hash, contatori ciclici
overflowing_add e simili (T, bool) Vuoi il valore dopo il giro e anche sapere che c’è stato
T::try_from(x) Result<T, _> Converti un valore che potrebbe non stare in un tipo più piccolo

len() restituisce un usize, che non può essere negativo. Su una lista vuota, len() - 1 farebbe -1, quindi va in overflow:

fn last_index(tasks: &[String]) -> usize {
tasks.len() - 1
}
fn main() {
let tasks: Vec<String> = Vec::new();
println!("l'ultima attività è all'indice {}", last_index(&tasks));
}

Quando lo esegui, ottieni:

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
▶ Provalo tu

Questa volta è un «tentativo di sottrazione con overflow». Ci cascano quasi tutti almeno una volta. La soluzione è lasciare che sia il tipo a dirti che la lista potrebbe essere vuota: restituisci tasks.len().checked_sub(1), che è un Option<usize>. Se vuoi solo l’ultimo elemento, tasks.last() è ancora più semplice.

Tre voti da 90, 85 e 100 stanno ciascuno in un u8. Il loro totale, 275, no. Quindi let total: u8 = scores.iter().sum(); va in panic con attempt to add with overflow. Il panic punta dentro la libreria standard, e questo può confondere, ma la causa è il tuo u8. Converti ogni valore in un tipo più grande prima di sommare:

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

Output:

totale: 275, media: 91
▶ Provalo tu

Passare da u8 a u32 con as è sempre sicuro, perché ogni u8 sta in un u32.

Il livello della batteria di un telefono è un u8 da 0 a 100. Scrivi charge(level: u8, amount: u8) -> u8 e drain(level: u8, amount: u8) -> u8 in modo che non vadano mai sopra 100 o sotto 0. Parti da 80, ricarica di 30, poi scarica di 130, e stampa il livello dopo ogni passo. Suggerimento: saturating_add più .min(100) per ricaricare, saturating_sub per scaricare.

Mostra la soluzione
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!("inizio: {level}%");
level = charge(level, 30);
println!("dopo la ricarica: {level}%");
level = drain(level, 130);
println!("dopo una lunga giornata: {level}%");
}

Output:

inizio: 80%
dopo la ricarica: 100%
dopo una lunga giornata: 0%
▶ Provalo tu

saturating_sub si ferma a 0 da solo. Per la ricarica, il limite del tipo è 255, quindi .min(100) aggiunge sopra il tuo limite.

Scrivi fn factorial(n: u64) -> Option<u64> che moltiplica 1 × 2 × … × n e restituisce None se il risultato diventa troppo grande per un u64. Stampa il risultato per 5, 20 e 21. Suggerimento: parti da let mut result: u64 = 1; e usa result.checked_mul(i)? dentro un ciclo for.

Mostra la soluzione
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}! è troppo grande per un u64"),
}
}
}

Output:

5! = 120
20! = 2432902008176640000
21! è troppo grande per un u64
▶ Provalo tu

Il ? fa restituire None all’intera funzione nel momento in cui una moltiplicazione non ci sta. 20! è il fattoriale più grande che un u64 riesce a contenere.

  • Ogni tipo intero ha un intervallo fisso, da T::MIN a T::MAX. Uscirne è un overflow.
  • Le build di debug vanno in panic con attempt to add with overflow. Le build di release fanno il giro in silenzio. Un overflow che il compilatore vede è un errore di compilazione.
  • Scegli tu il comportamento: checked_ dà un Option, saturating_ si ferma al limite, wrapping_ fa il giro, overflowing_ dà il valore e un bool.
  • as non controlla mai e può cambiare un numero in silenzio. Usa T::try_from quando un valore potrebbe non starci.
  • Attenzione a len() - 1 sulle liste vuote, alle somme in tipi piccoli, ad abs() di MIN e a pow.

Verifica veloce

1. Cosa succede a 250u8 + 10 in una build di debug, quando i valori arrivano da variabili?

2. Cosa restituisce 100u64.checked_sub(250)?

3. Una barra della salute non deve mai scendere sotto 0. Quale metodo è il più adatto?

4. Quanto vale 300_i32 as u8?

Punteggio: 0 / 4