@subWithOverflow em Zig — Referência e Exemplos

@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): 1 se houve overflow/underflow, 0 caso 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

  1. Aritmética segura: Evitar panic por underflow em tipos unsigned.
  2. Saturação: Implementar subtração saturante (clampar em zero).
  3. Diferença absoluta: Calcular distância entre valores sem saber a ordem.
  4. 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

Tutoriais relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.