@subWithOverflow em Zig
O @subWithOverflow realiza uma subtração e indica se houve overflow (ou underflow). Retorna uma tupla com o resultado (possivelmente truncado) e um bit de overflow. Especialmente útil com tipos unsigned, onde subtrair um valor maior causa underflow.
Sintaxe
@subWithOverflow(a: T, b: T) struct { T, u1 }
Parâmetros
- a (
T): Minuendo (tipo inteiro). - b (
T): Subtraendo (mesmo tipo inteiro).
Valor de retorno
Retorna uma tupla struct { T, u1 }:
- [0] (
T): Resultado da subtração (truncado se overflow). - [1] (
u1):1se houve overflow/underflow,0caso contrário.
Exemplos práticos
Exemplo 1: Detecção de underflow em unsigned
const std = @import("std");
pub fn main() void {
// Sem underflow
const r1 = @subWithOverflow(@as(u8, 100), @as(u8, 50));
std.debug.print("100 - 50 = {} (overflow: {})\n", .{ r1[0], r1[1] });
// 100 - 50 = 50 (overflow: 0)
// Com underflow: 50 - 100 em u8
const r2 = @subWithOverflow(@as(u8, 50), @as(u8, 100));
std.debug.print("50 - 100 = {} (overflow: {})\n", .{ r2[0], r2[1] });
// 50 - 100 = 206 (overflow: 1) — wraps around
}
Exemplo 2: Subtração segura com fallback
const std = @import("std");
fn subSegura(a: u32, b: u32) u32 {
const resultado = @subWithOverflow(a, b);
if (resultado[1] != 0) {
return 0; // Saturar em zero em vez de overflow
}
return resultado[0];
}
pub fn main() void {
std.debug.print("{}\n", .{subSegura(100, 30)}); // 70
std.debug.print("{}\n", .{subSegura(10, 50)}); // 0 (saturado)
}
Exemplo 3: Cálculo de diferença absoluta
const std = @import("std");
fn diferencaAbsoluta(a: u32, b: u32) u32 {
const r = @subWithOverflow(a, b);
if (r[1] != 0) {
// a < b, calcular b - a
return b - a;
}
return r[0];
}
pub fn main() void {
std.debug.print("|100 - 30| = {}\n", .{diferencaAbsoluta(100, 30)}); // 70
std.debug.print("|30 - 100| = {}\n", .{diferencaAbsoluta(30, 100)}); // 70
}
Casos de uso comuns
- Aritmética segura: Evitar panic por underflow em tipos unsigned.
- Saturação: Implementar subtração saturante (clampar em zero).
- Diferença absoluta: Calcular distância entre valores sem saber a ordem.
- Timers e contadores: Detectar wrap-around em contadores que decrementam.
Underflow vs. Overflow: a diferença para tipos unsigned
Para tipos unsigned, subtrair um valor maior causa underflow — o resultado “dá a volta” pelo zero para o valor máximo do tipo. Por exemplo, 0u8 - 1 resulta em 255. O flag de overflow retornado por @subWithOverflow detecta exatamente esse caso.
Para tipos signed, overflow ocorre quando o resultado sai do intervalo representável em ambas as direções: i8_min - 1 causa overflow positivo e i8_max + 1 causa overflow negativo.
Considerações de desempenho
Como @mulWithOverflow e @addWithOverflow, @subWithOverflow compila para uma única instrução de subtração seguida de verificação do carry flag ou overflow flag do processador. Em x86-64, isso é sub com verificação de CF (unsigned) ou OF (signed). O custo é negligível comparado a qualquer verificação manual equivalente.
Subtração saturante vs. com overflow
Existem dois padrões comuns ao lidar com subtração que pode underflow:
const std = @import("std");
// 1. Subtração saturante: clampeia em zero
fn subSaturante(a: u32, b: u32) u32 {
const r = @subWithOverflow(a, b);
return if (r[1] != 0) 0 else r[0];
}
// 2. Subtração com erro: retorna erro em caso de underflow
fn subComErro(a: u32, b: u32) !u32 {
const r = @subWithOverflow(a, b);
if (r[1] != 0) return error.Underflow;
return r[0];
}
// 3. Diferença absoluta: sempre positivo
fn difAbsoluta(a: u32, b: u32) u32 {
if (a >= b) return a - b;
return b - a;
}
Comparação com equivalente em C
Em C, detectar underflow de subtração unsigned requer comparação prévia:
// C: verificação manual (correta mas verbosa)
unsigned int sub_segura(unsigned int a, unsigned int b) {
if (b > a) return 0; // underflow: saturar em zero
return a - b;
}
// C alternativa: usando wrapping e comparação posterior (menos intuitivo)
unsigned int resultado = a - b;
if (resultado > a) { /* underflow ocorreu */ }
Em Zig, @subWithOverflow é direto e não requer comparação prévia:
fn subSegura(a: u32, b: u32) u32 {
const r = @subWithOverflow(a, b);
return if (r[1] != 0) 0 else r[0];
}
Erros comuns
1. Usar subtração direta com tipos unsigned quando underflow é possível:
// PERIGOSO em Debug: panic se b > a
// UNDEFINED BEHAVIOR em ReleaseFast
const resultado = a - b;
// CORRETO: usar @subWithOverflow para tratar o caso
const r = @subWithOverflow(a, b);
if (r[1] != 0) return error.Underflow;
return r[0];
2. Confundir overflow e underflow: Para unsigned, subtrair mais do que o valor disponível é underflow (não overflow, tecnicamente). O Zig usa o termo “overflow” para ambos, pois a flag do hardware não distingue — ela indica apenas que o resultado não cabe no tipo.
Perguntas Frequentes
P: Para tipos signed, @subWithOverflow detecta overflow nos dois sentidos?
R: Sim. Para i8, tanto 127 - (-1) (resultado 128, acima de i8_max) quanto -128 - 1 (resultado -129, abaixo de i8_min) retornam flag de overflow igual a 1.
P: É possível usar @subWithOverflow para implementar contadores que decrementam com wrap-around?
R: Sim — e o resultado truncado em [0] é exatamente o valor com wrap. Você pode verificar com o flag quando o contador “deu a volta” e resetar estados conforme necessário.
P: @subWithOverflow funciona com usize para aritmética de ponteiros?
R: Sim. usize é tratado como qualquer outro tipo unsigned. Mas para aritmética de ponteiros, é mais idiomático usar as funções de std.mem ou aritmética de ponteiros direta com verificações de bounds.
Builtins relacionados
- @addWithOverflow — Adição com detecção de overflow
- @mulWithOverflow — Multiplicação com detecção de overflow
- @min / @max — Limitar valores
- @shlExact — Shift left exato