Allocator em Zig — O que é e Como Usar

Allocator em Zig — O que é e Como Usar

Definição

Um Allocator (alocador) em Zig é uma abstração que encapsula a lógica de alocação e desalocação de memória. Diferentemente de linguagens como C, onde você usa diretamente malloc e free, ou de linguagens com garbage collector como Java e Go, Zig oferece uma interface padronizada (std.mem.Allocator) que permite trocar a estratégia de alocação sem alterar o código que a consome.

Em termos simples, o allocator é um “objeto” que sabe como pedir memória ao sistema e devolvê-la. Toda função da biblioteca padrão que precisa alocar memória recebe um allocator como parâmetro — essa é uma decisão de design fundamental da linguagem.

Por que Allocators Importam

  1. Controle explícito: Você sempre sabe onde e quando memória é alocada.
  2. Testabilidade: Em testes, você pode usar um allocator que detecta memory leaks automaticamente.
  3. Performance: Diferentes situações pedem diferentes estratégias (arena, pool, page).
  4. Sem alocação oculta: Nenhuma função aloca memória “por baixo dos panos” sem que você saiba.

Exemplo Prático

const std = @import("std");

pub fn main() !void {
    // Criando um General Purpose Allocator
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) @panic("Vazamento de memória detectado!");
    }

    const allocator = gpa.allocator();

    // Alocando uma lista dinâmica
    var lista = std.ArrayList(u32).init(allocator);
    defer lista.deinit();

    try lista.append(10);
    try lista.append(20);
    try lista.append(30);

    for (lista.items) |valor| {
        std.debug.print("Valor: {}\n", .{valor});
    }
}

Neste exemplo, o GeneralPurposeAllocator é criado, e sua interface allocator() é passada para o ArrayList. O defer garante que a memória será liberada ao final do escopo.

Como Funciona a Interface

A interface std.mem.Allocator define três operações fundamentais:

// Simplificação da interface
pub const Allocator = struct {
    allocFn: fn (self: *anyopaque, len: usize, ...) ?[*]u8,
    resizeFn: fn (self: *anyopaque, buf: []u8, new_len: usize, ...) ?usize,
    freeFn: fn (self: *anyopaque, buf: []u8, ...) void,
};

Qualquer tipo que implemente essas funções pode ser usado como allocator. Isso permite criar alocadores personalizados para situações específicas.

Exemplo com ArenaAllocator

O ArenaAllocator é ideal para operações onde muitos objetos são criados juntos e liberados de uma só vez — como processar uma requisição HTTP ou fazer parsing de um arquivo:

const std = @import("std");

fn processarRequisicao(alocador_base: std.mem.Allocator, dados: []const u8) !void {
    // Arena vive enquanto a requisição durar
    var arena = std.heap.ArenaAllocator.init(alocador_base);
    defer arena.deinit(); // libera tudo de uma vez no final

    const alloc = arena.allocator();

    // Múltiplas alocações sem se preocupar com cada free
    const copia = try alloc.dupe(u8, dados);
    var lista = std.ArrayList([]const u8).init(alloc);
    try lista.append(copia);

    // Todas as alocações acima são liberadas automaticamente
    // quando arena.deinit() é chamado pelo defer
    _ = lista;
}

Passando Allocator para Funções

A convenção em Zig é que funções que precisam alocar memória recebem o allocator como primeiro ou segundo parâmetro:

// Convenção: allocator como parâmetro explícito
fn criarMensagem(allocator: std.mem.Allocator, nome: []const u8) ![]u8 {
    return std.fmt.allocPrint(allocator, "Olá, {s}!", .{nome});
}

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

    const msg = try criarMensagem(alloc, "Mundo");
    defer alloc.free(msg);
    std.debug.print("{s}\n", .{msg});
}

Essa convenção torna explícito que a função aloca memória, e o chamador controla qual estratégia usar — testando com std.testing.allocator ou em produção com um arena.

Comparação com Outras Linguagens

AbordagemLinguagemControlePrevisibilidade
Allocators explícitosZigTotalAlta
malloc/freeCTotalBaixa (sem abstração)
Garbage CollectorGo, JavaNenhumBaixa (GC pauses)
Ownership/borrowRustTotalAlta
Smart pointersC++ParcialMédia

A principal vantagem do modelo Zig é que o allocator é uma abstração testável e substituível — o mesmo código pode rodar com um allocator de produção ou com std.testing.allocator (que detecta leaks automaticamente em testes).

Armadilhas Comuns

  • Esquecer o defer deinit(): Sempre que alocar, garanta a desalocação com defer. Sem isso, você terá vazamentos de memória.
  • Usar o allocator errado: Em contextos de alta performance com muitas alocações curtas, prefira um ArenaAllocator em vez do GeneralPurposeAllocator.
  • Misturar allocators: Não aloque com um allocator e desaloque com outro. O comportamento é indefinido.
  • Ignorar falhas de alocação: alloc() retorna um error union. Sempre trate o possível erro com try ou catch.

Tipos de Allocator Disponíveis

AllocatorUso recomendado
GeneralPurposeAllocatorUso geral, com detecção de bugs
ArenaAllocatorMuitas alocações, uma única liberação
FixedBufferAllocatorSem alocação do sistema, usa buffer fixo
page_allocatorAlocações grandes, alinhadas a páginas

Termos Relacionados

Tutoriais Relacionados

Continue aprendendo Zig

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