@mulWithOverflow em Zig — Referência e Exemplos

@mulWithOverflow em Zig

O @mulWithOverflow realiza uma multiplicação e indica se houve overflow. Retorna uma tupla com o resultado (possivelmente truncado) e um bit de overflow. Multiplicação é especialmente propensa a overflow — dois valores que cabem em u32 podem facilmente produzir resultado que não cabe.

Sintaxe

@mulWithOverflow(a: T, b: T) struct { T, u1 }

Parâmetros

  • a (T): Primeiro fator (tipo inteiro).
  • b (T): Segundo fator (mesmo tipo inteiro).

Valor de retorno

Retorna uma tupla struct { T, u1 }:

  • [0] (T): Resultado da multiplicação (truncado se overflow).
  • [1] (u1): 1 se houve overflow, 0 caso contrário.

Exemplos práticos

Exemplo 1: Detecção de overflow na multiplicação

const std = @import("std");

pub fn main() void {
    // Sem overflow
    const r1 = @mulWithOverflow(@as(u8, 10), @as(u8, 20));
    std.debug.print("10 * 20 = {} (overflow: {})\n", .{ r1[0], r1[1] });
    // 10 * 20 = 200 (overflow: 0)

    // Com overflow: 200 * 2 = 400, mas u8 vai até 255
    const r2 = @mulWithOverflow(@as(u8, 200), @as(u8, 2));
    std.debug.print("200 * 2 = {} (overflow: {})\n", .{ r2[0], r2[1] });
    // 200 * 2 = 144 (overflow: 1)  — 400 mod 256 = 144
}

Exemplo 2: Alocação segura (verificar tamanho)

const std = @import("std");

fn alocarMatriz(allocator: std.mem.Allocator, linhas: usize, colunas: usize) ![]f64 {
    // Verificar se linhas * colunas causa overflow
    const total = @mulWithOverflow(linhas, colunas);
    if (total[1] != 0) {
        return error.TamanhoExcessivo;
    }

    return try allocator.alloc(f64, total[0]);
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const matriz = try alocarMatriz(gpa.allocator(), 100, 100);
    defer gpa.allocator().free(matriz);

    std.debug.print("Alocado: {} elementos\n", .{matriz.len});
}

Exemplo 3: Conversão de unidades com verificação

const std = @import("std");

fn kbParaBytes(kb: u32) !u32 {
    const resultado = @mulWithOverflow(kb, @as(u32, 1024));
    if (resultado[1] != 0) return error.Overflow;
    return resultado[0];
}

fn mbParaBytes(mb: u32) !u32 {
    const kb = @mulWithOverflow(mb, @as(u32, 1024));
    if (kb[1] != 0) return error.Overflow;
    return kbParaBytes(kb[0]);
}

pub fn main() void {
    const bytes = kbParaBytes(512) catch 0;
    std.debug.print("512 KB = {} bytes\n", .{bytes});

    const grande = mbParaBytes(5000) catch |err| {
        std.debug.print("Erro: {s}\n", .{@errorName(err)});
        return;
    };
    _ = grande;
}

Casos de uso comuns

  1. Alocação de memória: Verificar se N * sizeof(T) não causa overflow antes de alocar.
  2. Conversão de unidades: KB para bytes, minutos para milissegundos, etc.
  3. Cálculos financeiros: Multiplicar valores monetários por quantidades.
  4. Dimensionamento de buffers: Calcular tamanho total de matrizes/grids.

Considerações de desempenho

Em arquiteturas modernas, @mulWithOverflow é compilado para uma única instrução de multiplicação seguida de verificação do carry/overflow flag. Em x86-64, isso se traduz em imul (para signed) ou mul (para unsigned) com verificação de overflow, sendo mais eficiente do que qualquer alternativa em software.

Para tipos como usize, o compilador usa as instruções nativas de 64 bits, sem overhead extra. O custo é essencialmente o mesmo de uma multiplicação comum, com a adição de uma instrução de verificação de flag.

Comparação com equivalente em C

Em C, detectar overflow de multiplicação requer código manual e é propenso a erros:

// C: verificação manual de overflow (frágil e verbosa)
#include <limits.h>

int multiplicar_seguro(unsigned int a, unsigned int b, unsigned int *resultado) {
    if (a != 0 && b > UINT_MAX / a) {
        return 1; // overflow
    }
    *resultado = a * b;
    return 0;
}

Em Zig, a detecção é direta, atômica e não requer divisão como verificação prévia:

// Zig: claro e eficiente
const resultado = @mulWithOverflow(a, b);
if (resultado[1] != 0) return error.Overflow;
return resultado[0];

Erros comuns

1. Ignorar o flag de overflow:

// ERRADO: ignora o flag — pode produzir resultado incorreto silenciosamente
const resultado = @mulWithOverflow(a, b)[0];

// CORRETO: sempre verificar o flag
const r = @mulWithOverflow(a, b);
if (r[1] != 0) return error.Overflow;
const resultado = r[0];

2. Usar tipo errado para os operandos:

// ERRADO: mistura de tipos
// const r = @mulWithOverflow(@as(u32, a), @as(u64, b)); // erro de compilação

// CORRETO: ambos os operandos devem ter o mesmo tipo
const r = @mulWithOverflow(@as(u64, a), @as(u64, b));

Perguntas Frequentes

P: Qual é a diferença entre @mulWithOverflow e multiplicação com *?

R: O operador * em modo Debug/ReleaseSafe causa panic em caso de overflow. Em ReleaseFast, resulta em undefined behavior. O @mulWithOverflow sempre retorna o resultado truncado junto com a flag de overflow, permitindo que o programador trate o caso explicitamente sem panic e sem undefined behavior.

P: Posso usar @mulWithOverflow com tipos signed (i32, i64)?

R: Sim. Para tipos signed, overflow ocorre quando o resultado não cabe no intervalo representável (ex: i8 vai de -128 a 127). O comportamento de wrapping segue a mesma regra de truncamento em complemento de dois.

P: É possível usar @mulWithOverflow em comptime?

R: Sim, o builtin funciona em comptime. Isso é útil para verificações estáticas de valores conhecidos em tempo de compilação, como validar constantes de configuração.

Builtins relacionados

Tutoriais relacionados

Continue aprendendo Zig

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