Le dépassement d'entier en Rust
En Rust, chaque type entier a une taille fixe. Un u8 a de la place pour les nombres de 0 à 255, et rien d’autre. Alors que se passe-t-il si vous avez 255 et que vous ajoutez 1 ? Le résultat ne rentre pas. C’est ce qu’on appelle un dépassement d’entier (integer overflow).
Pensez au compteur kilométrique d’une vieille voiture qui n’aurait que trois chiffres. Après 999 kilomètres, il repasse à 000. Pour un jouet, ce n’est pas grave ; pour le solde d’un compte en banque, c’est catastrophique. Rust vous aide de deux façons. Il signale le problème haut et fort pendant que vous développez. Et il vous donne des méthodes simples pour dire exactement ce qui doit se passer à la place. Pas d’inquiétude : il n’y en a que quatre familles, et elles suivent toutes le même principe. Cette page s’appuie sur Les types de données et Les opérateurs.
Quelle est la taille de chaque type entier ?
Section intitulée « Quelle est la taille de chaque type entier ? »Chaque type a une plus petite valeur (MIN) et une plus grande valeur (MAX). Vous pouvez les afficher avec des constantes comme u8::MAX ou i32::MIN :
fn main() { println!("u8: {} à {}", u8::MIN, u8::MAX); println!("i8: {} à {}", i8::MIN, i8::MAX); println!("u16: {} à {}", u16::MIN, u16::MAX); println!("i32: {} à {}", i32::MIN, i32::MAX); println!("u32: {} à {}", u32::MIN, u32::MAX); println!("i64: {} à {}", i64::MIN, i64::MAX); println!("u64: {} à {}", u64::MIN, u64::MAX); println!("u128: {} à {}", u128::MIN, u128::MAX);}Sortie :
u8: 0 à 255i8: -128 à 127u16: 0 à 65535i32: -2147483648 à 2147483647u32: 0 à 4294967295i64: -9223372036854775808 à 9223372036854775807u64: 0 à 18446744073709551615u128: 0 à 340282366920938463463374607431768211455Voici toute la famille dans un seul tableau :
| Type | Bits | Plus petite valeur | Plus grande valeur |
|---|---|---|---|
i8 / u8 |
8 | -128 / 0 | 127 / 255 |
i16 / u16 |
16 | -32 768 / 0 | 32 767 / 65 535 |
i32 / u32 |
32 | environ -2,1 milliards / 0 | environ 2,1 milliards / 4,3 milliards |
i64 / u64 |
64 | environ -9,2 × 10^18 / 0 | environ 9,2 × 10^18 / 1,8 × 10^19 |
i128 / u128 |
128 | environ -1,7 × 10^38 / 0 | environ 1,7 × 10^38 / 3,4 × 10^38 |
isize / usize |
64 sur la plupart des ordinateurs | comme i64 / 0 |
comme i64 / u64 |
Un type signé (i) consacre la moitié de sa plage aux nombres négatifs. Un type non signé (u) commence à 0, donc sa plus grande valeur est environ deux fois plus grande. Tout ce qui sort de ces plages est un dépassement, et descendre sous MIN (comme 0 - 1 sur un u8) compte aussi. On parle parfois de dépassement par le bas (underflow), mais Rust le traite exactement de la même façon.
Ce que fait Rust quand un nombre déborde
Section intitulée « Ce que fait Rust quand un nombre déborde »En mode debug, il déclenche une panic
Section intitulée « En mode debug, il déclenche une panic »Quand vous lancez votre programme avec cargo run (une compilation en mode debug), Rust vérifie chaque +, - et *. Si le résultat ne rentre pas, le programme s’arrête avec un message clair. Ici, un jeu ajoute des points de bonus à un score de type u8 :
fn add_points(score: u8, bonus: u8) -> u8 { score + bonus}
fn main() { let total = add_points(250, 10); println!("total : {total}");}À l’exécution, vous obtenez :
thread 'main' (12345) panicked at src/main.rs:2:5:attempt to add with overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceLe message de Rust (en anglais) dit que le programme a « paniqué » à la ligne 2, colonne 5, en tentant une addition qui déborde. Les valeurs passent volontairement par des paramètres de fonction. Si vous écriviez les nombres juste à côté du +, le compilateur repérerait le problème avant même que le programme ne s’exécute (vous le verrez dans un instant). Un plantage peut sembler brutal, mais c’est un service qu’on vous rend : vous trouvez le bug sur votre propre machine, avec le numéro de ligne exact, au lieu d’obtenir un score faux en production. La page Panic en Rust en dit plus sur les panics (paniques).
En mode release, le nombre fait le tour
Section intitulée « En mode release, le nombre fait le tour »Quand vous compilez avec cargo run --release, ces vérifications sont désactivées pour rendre le code un peu plus rapide. Le nombre fait le tour (wrapping), comme le compteur kilométrique : 250 + 10 sur un u8 donne 4 (car 260 - 256 = 4).
cargo run --releasetotal : 4Pas de panic, pas d’avertissement, juste un nombre faux, en silence. Voilà pourquoi il ne faut pas compter sur un dépassement « par accident ». Si vous voulez que le nombre fasse le tour, dites-le avec wrapping_add (présenté plus bas). Les boutons « Essayez par vous-même » de ce site lancent des compilations en mode debug : wrapping_add est donc aussi le moyen d’y voir ce comportement :
fn main() { let score: u8 = 250; println!("{}", score.wrapping_add(10));}Sortie :
4Si vous voulez que les compilations release déclenchent aussi une panic, vous pouvez réactiver les vérifications dans Cargo.toml :
[profile.release]overflow-checks = trueAvec des nombres écrits en dur, ça ne compile même pas
Section intitulée « Avec des nombres écrits en dur, ça ne compile même pas »Quand le compilateur voit tout le calcul, il refuse de le compiler :
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 defaultLe compilateur prévient que cette opération arithmétique va déborder. C’est la même chose pour un littéral tout simplement trop grand, comme let level: u8 = 256;, qui donne literal out of range for u8 (littéral hors de la plage d’un u8).
Choisir vous-même le comportement
Section intitulée « Choisir vous-même le comportement »Plutôt que d’espérer qu’un dépassement n’arrive jamais, vous pouvez choisir ce qui doit se passer. Chaque type entier propose quatre familles de méthodes pour ça. Elles fonctionnent de la même manière sur u8, i32, u64 et tous les autres.
checked_ : « dis-moi si ça ne rentre pas »
Section intitulée « checked_ : « dis-moi si ça ne rentre pas » »checked_add, checked_sub et checked_mul renvoient une Option : Some(résultat) si ça rentre, None sinon. C’est le choix le plus sûr dès qu’un nombre faux poserait un vrai problème, comme pour de l’argent. Un solde bancaire ne peut pas descendre sous 0, donc un u64 avec checked_sub refuse le retrait à votre place :
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!("Retrait de {amount}, le solde est maintenant de {left}"), None => println!("Impossible de retirer {amount} : solde insuffisant"), } }
println!("{:?}", order_total(20, 3, 5)); println!("{:?}", order_total(2_000_000_000, 3, 5));}Sortie :
Retrait de 30, le solde est maintenant de 70Impossible de retirer 250 : solde insuffisantSome(65)NoneComme le résultat est une Option, l’opérateur ? fonctionne aussi. Dans order_total, si la multiplication déborde, la fonction renvoie None immédiatement.
saturating_ : « arrête-toi au bord »
Section intitulée « saturating_ : « arrête-toi au bord » »saturating_add et saturating_sub ne dépassent jamais MIN ni MAX. Elles s’arrêtent simplement là, comme un verre déjà plein. C’est parfait pour tout ce qui a un plancher ou un plafond naturel : une barre de santé, un bouton de volume, des articles en stock.
fn main() { let health: u8 = 20; let after_hit = health.saturating_sub(35); println!("santé après un gros coup : {after_hit}");
let volume: u8 = 90; let louder = volume.saturating_add(25).min(100); println!("volume après l'avoir monté : {louder}");
let stock: u32 = 3; let left = stock.saturating_sub(5); println!("articles restants en stock : {left}");}Sortie :
santé après un gros coup : 0volume après l'avoir monté : 100articles restants en stock : 0La ligne du volume demande une étape de plus. saturating_add ne s’arrête qu’à la limite du type lui-même, 255 pour un u8. Votre bouton s’arrête à 100, donc .min(100) ramène la valeur à votre propre limite.
wrapping_ : « fais le tour exprès »
Section intitulée « wrapping_ : « fais le tour exprès » »Parfois, faire le tour est exactement ce que vous voulez. Un distributeur de tickets qui fait 254, 255, 0, 1. Ou une fonction de hachage simple, où les nombres sont justement censés être mélangés. wrapping_add et wrapping_mul font ça dans toutes les compilations, debug comme 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 de \"hello\" : {}", simple_hash("hello")); println!("hash de \"a much longer sentence\" : {}", simple_hash("a much longer sentence"));}Sortie :
ticket 254ticket 255ticket 0ticket 1hash de "hello" : 261238937hash de "a much longer sentence" : 3373490575Les textes hachés restent en anglais, car le hash est calculé à partir de leurs octets : une lettre accentuée prend 2 octets en UTF-8 et changerait le résultat. Avec de simples * et +, la longue phrase ferait déborder un u32 de nombreuses fois. Avec les méthodes wrapping_, ce n’est pas un problème : c’est justement ce que le hash est censé faire.
overflowing_ : « donne-moi les deux »
Section intitulée « overflowing_ : « donne-moi les deux » »overflowing_add renvoie un tuple : le résultat après le tour et un bool qui indique s’il y a eu dépassement. C’est pratique quand vous voulez la valeur après le tour, mais aussi savoir que ça s’est produit :
fn main() { let (value, overflowed) = 250u8.overflowing_add(10); println!("{value}, dépassement : {overflowed}");
let (value, overflowed) = 50u8.overflowing_add(10); println!("{value}, dépassement : {overflowed}");}Sortie :
4, dépassement : true60, dépassement : falseVous utiliserez celle-ci moins souvent que les autres. Si vous ne savez pas laquelle choisir, checked_ est un excellent choix par défaut.
Le dépassement lors d’une conversion avec as
Section intitulée « Le dépassement lors d’une conversion avec as »Convertir entre types avec as ne déclenche jamais de panic, même en mode debug. Si la valeur ne rentre pas, elle est discrètement tronquée pour rentrer. Ça surprend beaucoup de monde, alors soyez prudent :
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!("ça rentre : {small}"), Err(e) => println!("300 ne rentre pas dans un u8 : {e}"), }
match u8::try_from(200) { Ok(small) => println!("ça rentre : {small}"), Err(e) => println!("200 ne rentre pas dans un u8 : {e}"), }}Sortie :
300 as u8 = 44-1 as u32 = 4294967295300 ne rentre pas dans un u8 : out of range integral type conversion attemptedça rentre : 200Le message d’erreur vient de Rust et reste en anglais : il signale une tentative de conversion d’entier hors de la plage autorisée. 300 as u8 ne garde que les 8 bits de poids faible, ce qui donne 44. -1 as u32 devient le plus grand u32. Ni l’un ni l’autre ne vous prévient. u8::try_from vérifie d’abord et vous donne un Result, pour que vous décidiez quoi faire. Une bonne habitude : n’utilisez as que si vous savez que la valeur rentre, et try_from quand elle vient de l’extérieur de votre programme. (Les nombres à virgule se comportent autrement : 300.7 as u8 s’arrête à 255 au lieu de faire le tour.) Les pages Conversion de types et Les conversions vont plus loin.
Autres surprises avec les nombres
Section intitulée « Autres surprises avec les nombres »Quelques autres pièges classiques, réunis au même endroit :
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));}Sortie :
7 / 2 = 3-7 / 2 = -3i32::MIN.checked_abs() = Nonei32::MIN.unsigned_abs() = 214748364810u32.checked_pow(9) = Some(1000000000)10u32.checked_pow(10) = None- La division entière laisse tomber le reste. Elle n’arrondit pas : elle coupe vers zéro, donc
-7 / 2vaut-3. Utilisez%pour obtenir le reste, ou des nombres à virgule si vous voulez des décimales. abs()peut déborder. Uni32descend jusqu’à -2147483648 mais ne monte que jusqu’à 2147483647 : la version positive dei32::MINne rentre donc pas.i32::MIN.abs()déclenche une panic en mode debug avecattempt to negate with overflow. Utilisezchecked_abs(), ouunsigned_abs(), qui renvoie unu32, où la valeur rentre.powgrandit vite. 10 puissance 10 est déjà trop grand pour unu32. Il existe aussichecked_pow,saturating_powetwrapping_pow.
Si une valeur doit toujours faire le tour ou toujours saturer, vous pouvez l’envelopper dans std::num::Wrapping ou std::num::Saturating. Les simples + et - font alors ce qu’il faut, et vous ne pouvez plus oublier :
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!("batterie : {battery}");}Sortie :
ticket : 1batterie : 0Quelle méthode utiliser ?
Section intitulée « Quelle méthode utiliser ? »| Famille de méthodes | Renvoie | À utiliser quand |
|---|---|---|
+ - * |
Le nombre (panic en debug, fait le tour en release) | Vous êtes sûr qu’il n’y aura pas de dépassement |
checked_add et compagnie |
Option<T> |
Un nombre faux serait un bug : argent, tailles, identifiants |
saturating_add et compagnie |
Le nombre, bloqué à MIN ou MAX |
Il existe un plancher ou un plafond naturel : santé, volume, stock |
wrapping_add et compagnie |
Le nombre, après avoir fait le tour | Faire le tour est le but : hachages, compteurs qui tournent en boucle |
overflowing_add et compagnie |
(T, bool) |
Vous voulez la valeur après le tour et savoir si le tour a eu lieu |
T::try_from(x) |
Result<T, _> |
Vous convertissez une valeur qui risque de ne pas rentrer dans un type plus petit |
Erreurs courantes
Section intitulée « Erreurs courantes »Soustraire 1 à une longueur nulle
Section intitulée « Soustraire 1 à une longueur nulle »len() renvoie un usize, qui ne peut pas être négatif. Sur une liste vide, len() - 1 vaudrait -1, donc il y a dépassement :
fn last_index(tasks: &[String]) -> usize { tasks.len() - 1}
fn main() { let tasks: Vec<String> = Vec::new(); println!("la dernière tâche est à l'indice {}", last_index(&tasks));}À l’exécution, vous obtenez :
thread 'main' (12345) panicked at src/main.rs:2:5:attempt to subtract with overflownote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceCette fois, la panic signale une soustraction qui déborde. Presque tout le monde tombe dans ce piège au moins une fois. La solution consiste à laisser le type vous dire que la liste peut être vide : renvoyez tasks.len().checked_sub(1), qui est une Option<usize>. Et si vous voulez juste le dernier élément, tasks.last() est encore plus simple.
Additionner de petits nombres dans un petit type
Section intitulée « Additionner de petits nombres dans un petit type »Trois notes de 90, 85 et 100 rentrent chacune dans un u8. Leur total, 275, ne rentre pas. Du coup, let total: u8 = scores.iter().sum(); déclenche une panic avec attempt to add with overflow. La panic pointe vers l’intérieur de la bibliothèque standard, ce qui peut dérouter, mais la cause, c’est bien votre u8. Convertissez chaque valeur vers un type plus grand avant de les additionner :
fn main() { let scores: Vec<u8> = vec![90, 85, 100]; let total: u32 = scores.iter().map(|&s| s as u32).sum(); println!("total : {total}, moyenne : {}", total / 3);}Sortie :
total : 275, moyenne : 91Passer de u8 à u32 avec as est toujours sans risque, car tout u8 rentre dans un u32.
Exercices
Section intitulée « Exercices »1. La batterie d’un téléphone
Section intitulée « 1. La batterie d’un téléphone »Le niveau de batterie d’un téléphone est un u8 de 0 à 100. Écrivez charge(level: u8, amount: u8) -> u8 et drain(level: u8, amount: u8) -> u8, qui ne dépassent jamais 100 et ne descendent jamais sous 0. Partez de 80, chargez de 30, puis déchargez de 130, et affichez le niveau après chaque étape. Indice : saturating_add avec .min(100) pour la charge, saturating_sub pour la décharge.
Voir la solution
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!("départ : {level} %");
level = charge(level, 30); println!("après la charge : {level} %");
level = drain(level, 130); println!("après une longue journée : {level} %");}Sortie :
départ : 80 %après la charge : 100 %après une longue journée : 0 %saturating_sub s’arrête tout seul à 0. Pour la charge, la limite du type est 255, donc .min(100) ajoute votre propre limite par-dessus.
2. Une factorielle qui connaît ses limites
Section intitulée « 2. Une factorielle qui connaît ses limites »Écrivez fn factorial(n: u64) -> Option<u64>, qui calcule 1 × 2 × … × n et renvoie None si le résultat devient trop grand pour un u64. Affichez le résultat pour 5, 20 et 21. Indice : commencez par let mut result: u64 = 1; et utilisez result.checked_mul(i)? dans une boucle for.
Voir la solution
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}! est trop grand pour un u64"), } }}Sortie :
5! = 12020! = 243290200817664000021! est trop grand pour un u64Le ? fait renvoyer None à toute la fonction dès qu’une multiplication ne rentre pas. 20! est la plus grande factorielle qu’un u64 peut contenir.
Récapitulatif
Section intitulée « Récapitulatif »- Chaque type entier a une plage fixe, de
T::MINàT::MAX. En sortir, c’est un dépassement. - En mode debug, le programme déclenche une panic avec
attempt to add with overflow. En mode release, le nombre fait le tour sans rien dire. Un dépassement que le compilateur peut voir est une erreur de compilation. - Choisissez vous-même le comportement :
checked_donne uneOption,saturating_s’arrête au bord,wrapping_fait le tour,overflowing_donne la valeur et unbool. asne vérifie jamais rien et peut changer un nombre en silence. UtilisezT::try_fromquand une valeur risque de ne pas rentrer.- Méfiez-vous de
len() - 1sur une liste vide, des sommes dans de petits types, deabs()surMIN, et depow.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Que devient 250u8 + 10 en mode debug, quand les valeurs viennent de variables ?
2. Que renvoie 100u64.checked_sub(250) ?
3. Une barre de santé ne doit jamais descendre sous 0. Quelle méthode convient le mieux ?
4. Que vaut 300_i32 as u8 ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Integer Overflow — The Rust Book (en anglais) : l’explication officielle des panics en mode debug, du « tour » en mode release, et des quatre familles de méthodes.
u8— documentation de la bibliothèque standard (en anglais) : toutes les méthodeschecked_,saturating_,wrapping_etoverflowing_, avec un petit exemple pour chacune.i32— documentation de la bibliothèque standard (en anglais) : les mêmes méthodes pour les nombres signés, pluschecked_abs,unsigned_absetchecked_pow.- Overflow — The Rust Reference (en anglais) : les règles exactes sur les opérations qui peuvent déborder et ce qui se passe dans ce cas.
- Casting — Rust by Example (en anglais) : des exemples exécutables de conversions avec
asqui font le tour, tronquent ou saturent.