Pular para o conteúdo

Overflow de inteiros em Rust

Todo tipo inteiro em Rust tem um tamanho fixo. Um u8 tem espaço para os números de 0 a 255, e mais nada. Então o que acontece quando você tem 255 e soma 1? A resposta não cabe. Isso se chama integer overflow (estouro de inteiro), ou simplesmente overflow.

Pense no hodômetro de um carro antigo que só tem três dígitos. Depois de 999 quilômetros, ele volta para 000. Isso não tem problema num brinquedo, mas é péssimo no saldo de uma conta bancária. O Rust te ajuda aqui de duas formas. Ele pega o problema fazendo barulho enquanto você está desenvolvendo. E te dá métodos simples para dizer exatamente o que deve acontecer no lugar. Não se preocupe: são só quatro, e todos seguem o mesmo padrão. Esta página parte do que você viu em Tipos de dados e Operadores.

Cada tipo tem um menor valor (MIN) e um maior valor (MAX). Você pode imprimi-los com constantes como u8::MAX ou i32::MIN:

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

Saída:

u8: 0 a 255
i8: -128 a 127
u16: 0 a 65535
i32: -2147483648 a 2147483647
u32: 0 a 4294967295
i64: -9223372036854775808 a 9223372036854775807
u64: 0 a 18446744073709551615
u128: 0 a 340282366920938463463374607431768211455
▶ Experimente você mesmo

Aqui está a família inteira em uma tabela:

Tipo Bits Menor valor Maior valor
i8 / u8 8 -128 / 0 127 / 255
i16 / u16 16 -32.768 / 0 32.767 / 65.535
i32 / u32 32 cerca de -2,1 bilhões / 0 cerca de 2,1 bilhões / 4,3 bilhões
i64 / u64 64 cerca de -9,2 × 10^18 / 0 cerca de 9,2 × 10^18 / 1,8 × 10^19
i128 / u128 128 cerca de -1,7 × 10^38 / 0 cerca de 1,7 × 10^38 / 3,4 × 10^38
isize / usize 64 na maioria dos computadores igual ao i64 / 0 igual ao i64 / u64

Um tipo com sinal (i) gasta metade da sua faixa com números negativos. Um tipo sem sinal (u) começa em 0, então o seu maior valor é cerca de duas vezes maior. Qualquer coisa fora dessas faixas é overflow, e ficar abaixo de MIN (como 0 - 1 em um u8) também conta. Esse caso às vezes é chamado de underflow, mas o Rust trata do mesmo jeito.

Quando você roda o programa com cargo run (um build de debug), o Rust confere cada +, - e *. Se a resposta não cabe, o programa para com uma mensagem clara. Aqui um jogo soma pontos de bônus a uma pontuação u8:

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

Quando você roda, aparece:

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
▶ Experimente você mesmo

A mensagem attempt to add with overflow quer dizer “tentativa de somar com overflow”, e a linha de cima mostra o arquivo e a linha exata. Os valores passam por parâmetros de função de propósito. Se você escrevesse os números bem do lado do +, o compilador perceberia o problema antes mesmo de o programa rodar (você vai ver isso daqui a pouco). Um panic (parada do programa) pode parecer duro, mas é uma gentileza: você encontra o bug na sua própria máquina, com o número exato da linha, em vez de receber uma pontuação errada em produção. Tem mais sobre panics na página de Panic.

Quando você compila com cargo run --release, essas verificações são desligadas para deixar o código um pouco mais rápido. O número dá a volta como o hodômetro: 250 + 10 em um u8 vira 4 (porque 260 - 256 = 4).

Terminal window
cargo run --release
total: 4

Nada de panic, nada de aviso, só um número errado em silêncio. É por isso que você não deve contar com um overflow que acontece “sem querer”. Se você quer que o número dê a volta, diga isso com wrapping_add (mostrado mais abaixo). Os botões “Try it” deste site rodam builds de debug, então o wrapping_add também é o jeito de ver esse comportamento por lá:

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

Saída:

4
▶ Experimente você mesmo

Se você quiser que os builds de release também deem panic, pode religar as verificações no Cargo.toml:

[profile.release]
overflow-checks = true

Com números escritos direto no código, nem compila

Seção intitulada “Com números escritos direto no código, nem compila”

Quando o compilador consegue ver a conta inteira, ele se recusa a compilar:

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

O compilador avisa que esta operação aritmética vai estourar. O mesmo vale para um literal que é simplesmente grande demais, como let level: u8 = 256;, que dá literal out of range for u8 (“literal fora da faixa de u8”).

Em vez de torcer para o overflow nunca acontecer, você pode escolher o que deve acontecer. Todo tipo inteiro tem quatro famílias de métodos para isso. Todas funcionam do mesmo jeito em u8, i32, u64 e nos outros.

checked_add, checked_sub e checked_mul retornam um Option: Some(resposta) se couber, None se não couber. Essa é a escolha mais segura sempre que um número errado seria um problema de verdade, como com dinheiro. O saldo de uma conta não pode ficar abaixo de 0, então um u64 com checked_sub recusa o saque por você:

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!("Saque de {amount} feito, o saldo agora é {left}"),
None => println!("Não dá para sacar {amount}: saldo insuficiente"),
}
}
println!("{:?}", order_total(20, 3, 5));
println!("{:?}", order_total(2_000_000_000, 3, 5));
}

Saída:

Saque de 30 feito, o saldo agora é 70
Não dá para sacar 250: saldo insuficiente
Some(65)
None
▶ Experimente você mesmo

Como o resultado é um Option, o operador ? também funciona. Em order_total, se a multiplicação estourar, a função retorna None na hora.

saturating_add e saturating_sub nunca passam de MIN ou MAX. Eles simplesmente param ali, como um copo que já está cheio. Isso é perfeito para coisas que têm um piso ou um teto natural: uma barra de vida, um botão de volume, itens no estoque.

fn main() {
let health: u8 = 20;
let after_hit = health.saturating_sub(35);
println!("vida depois de um golpe forte: {after_hit}");
let volume: u8 = 90;
let louder = volume.saturating_add(25).min(100);
println!("volume depois de aumentar: {louder}");
let stock: u32 = 3;
let left = stock.saturating_sub(5);
println!("itens restantes no estoque: {left}");
}

Saída:

vida depois de um golpe forte: 0
volume depois de aumentar: 100
itens restantes no estoque: 0
▶ Experimente você mesmo

A linha do volume precisa de um passo a mais. O saturating_add só para no limite do próprio tipo, que é 255 para um u8. O seu botão para em 100, então o .min(100) prende o valor no seu próprio limite.

Às vezes dar a volta é exatamente o que você quer. Um contador de senhas que vai 254, 255, 0, 1. Ou uma função de hash simples, em que os números devem mesmo ficar embaralhados. wrapping_add e wrapping_mul fazem isso em qualquer build, de debug ou de 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!("senha {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"));
}

Saída:

senha 254
senha 255
senha 0
senha 1
hash de "hello": 261238937
hash de "a much longer sentence": 3373490575
▶ Experimente você mesmo

Os textos "hello" e "a much longer sentence" ficaram em inglês de propósito: o hash é calculado a partir dos bytes de cada texto, e letras acentuadas ocupam 2 bytes em UTF-8, então outro texto daria outros números. A frase longa estouraria um u32 muitas e muitas vezes com * e + comuns. Com os métodos wrapping_, tudo bem: é justamente isso que o hash deve fazer.

overflowing_add retorna uma tupla: a resposta que deu a volta e um bool que diz se houve overflow. É útil quando você quer o valor que deu a volta, mas também quer saber que isso aconteceu:

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

Saída:

4, estourou: true
60, estourou: false
▶ Experimente você mesmo

Você vai usar este bem menos do que os outros. Se não tiver certeza de qual escolher, o checked_ é um ótimo padrão.

Converter entre tipos com as nunca dá panic, nem em builds de debug. Se o valor não cabe, ele é cortado em silêncio até caber. Isso surpreende muita gente, então tome cuidado:

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!("cabe: {small}"),
Err(e) => println!("300 não cabe em um u8: {e}"),
}
match u8::try_from(200) {
Ok(small) => println!("cabe: {small}"),
Err(e) => println!("200 não cabe em um u8: {e}"),
}
}

Saída:

300 as u8 = 44
-1 as u32 = 4294967295
300 não cabe em um u8: out of range integral type conversion attempted
cabe: 200
▶ Experimente você mesmo

A mensagem out of range integral type conversion attempted vem do próprio Rust, por isso aparece em inglês: ela quer dizer “tentativa de conversão de inteiro fora da faixa”. 300 as u8 guarda só os 8 bits mais baixos, o que dá 44. -1 as u32 vira o maior u32. Nenhum dos dois te avisa. O u8::try_from confere antes e te dá um Result, para você decidir o que fazer. Um bom hábito: use as só quando você sabe que o valor cabe, e try_from quando ele vem de fora do seu programa. (Números de ponto flutuante se comportam diferente: 300.7 as u8 para em 255 em vez de dar a volta.) As páginas de Conversão de tipos e Conversões se aprofundam no assunto.

Mais algumas coisas que pegam as pessoas desprevenidas, todas num lugar só:

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

Saída:

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
▶ Experimente você mesmo
  • A divisão de inteiros descarta o resto. Ela não arredonda; ela corta em direção ao zero, então -7 / 2 dá -3. Use % para pegar o resto, ou números de ponto flutuante se quiser casas decimais.
  • O abs() pode estourar. Um i32 vai até -2147483648 para baixo, mas só até 2147483647 para cima, então a versão positiva de i32::MIN não cabe. i32::MIN.abs() dá panic em builds de debug com attempt to negate with overflow (“tentativa de inverter o sinal com overflow”). Use checked_abs() ou unsigned_abs(), que retorna um u32, onde o valor cabe.
  • O pow cresce rápido. 10 elevado a 10 já é grande demais para um u32. Também existem checked_pow, saturating_pow e wrapping_pow.

Se um valor deve sempre dar a volta ou sempre saturar, você pode embrulhá-lo em std::num::Wrapping ou std::num::Saturating. Aí o + e o - comuns fazem a coisa certa, e você não tem como esquecer:

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

Saída:

senha: 1
bateria: 0
▶ Experimente você mesmo
Família de métodos Retorna Use quando
+ - * O número (panic em debug, dá a volta em release) Você tem certeza de que não pode estourar
checked_add e companhia Option<T> Um número errado seria um bug: dinheiro, tamanhos, IDs
saturating_add e companhia O número, preso em MIN ou MAX Existe um piso ou teto natural: vida, volume, estoque
wrapping_add e companhia O número, depois de dar a volta Dar a volta é a ideia: hashes, contadores que recomeçam
overflowing_add e companhia (T, bool) Você quer o valor que deu a volta e saber que isso aconteceu
T::try_from(x) Result<T, _> Converter um valor que pode não caber em um tipo menor

len() retorna um usize, que não pode ser negativo. Em uma lista vazia, len() - 1 daria -1, então estoura:

fn last_index(tasks: &[String]) -> usize {
tasks.len() - 1
}
fn main() {
let tasks: Vec<String> = Vec::new();
println!("a última tarefa está no índice {}", last_index(&tasks));
}

Quando você roda, aparece:

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
▶ Experimente você mesmo

Aqui o panic diz “tentativa de subtrair com overflow”. Esse erro pega quase todo mundo pelo menos uma vez. A correção é deixar o tipo te avisar que a lista pode estar vazia: retorne tasks.len().checked_sub(1), que é um Option<usize>. Se você só quer o último item, tasks.last() é ainda mais simples.

Três notas de prova, 90, 85 e 100, cabem cada uma em um u8. O total delas, 275, não cabe. Então let total: u8 = scores.iter().sum(); dá panic com attempt to add with overflow. O panic aponta para dentro da biblioteca padrão, o que pode confundir, mas a causa é o seu u8. Converta cada valor para um tipo maior antes de somar:

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

Saída:

total: 275, média: 91
▶ Experimente você mesmo

Ir de u8 para u32 com as é sempre seguro, porque todo u8 cabe em um u32.

O nível da bateria de um celular é um u8 de 0 a 100. Escreva charge(level: u8, amount: u8) -> u8 e drain(level: u8, amount: u8) -> u8, que nunca passam de 100 nem ficam abaixo de 0. Comece em 80, carregue 30, depois gaste 130, e imprima o nível depois de cada passo. Dica: saturating_add mais .min(100) para carregar, saturating_sub para gastar.

Ver solução
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!("início: {level}%");
level = charge(level, 30);
println!("depois de carregar: {level}%");
level = drain(level, 130);
println!("depois de um dia longo: {level}%");
}

Saída:

início: 80%
depois de carregar: 100%
depois de um dia longo: 0%
▶ Experimente você mesmo

O saturating_sub para em 0 sozinho. Para carregar, o limite do tipo é 255, então o .min(100) coloca o seu próprio limite por cima.

Escreva fn factorial(n: u64) -> Option<u64>, que multiplica 1 × 2 × … × n e retorna None se a resposta ficar grande demais para um u64. Imprima o resultado para 5, 20 e 21. Dica: comece com let mut result: u64 = 1; e use result.checked_mul(i)? dentro de um laço for.

Ver solução
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}! é grande demais para um u64"),
}
}
}

Saída:

5! = 120
20! = 2432902008176640000
21! é grande demais para um u64
▶ Experimente você mesmo

O ? retorna None da função inteira no instante em que uma multiplicação não cabe. 20! é o maior fatorial que um u64 consegue guardar.

  • Todo tipo inteiro tem uma faixa fixa, de T::MIN a T::MAX. Sair dela é overflow.
  • Builds de debug dão panic com attempt to add with overflow. Builds de release dão a volta em silêncio. Um overflow que o compilador consegue ver é um erro de compilação.
  • Escolha o comportamento você mesmo: checked_ dá um Option, saturating_ para na borda, wrapping_ dá a volta, overflowing_ dá o valor e um bool.
  • O as nunca confere e pode mudar um número em silêncio. Use T::try_from quando um valor pode não caber.
  • Cuidado com len() - 1 em listas vazias, somas em tipos pequenos, abs() de MIN e pow.

Teste o que aprendeu

1. O que acontece com 250u8 + 10 em um build de debug, quando os valores vêm de variáveis?

2. O que 100u64.checked_sub(250) retorna?

3. Uma barra de vida nunca deve ficar abaixo de 0. Qual método se encaixa melhor?

4. Quanto vale 300_i32 as u8?

Pontuação: 0 / 4