Cheatsheet: Type Erasure em Zig

Type Erasure em Zig

Type Erasure é a técnica de “apagar” a informação de tipo concreto para criar interfaces genéricas que funcionam em runtime. Em linguagens com herança, isso é feito com classes abstratas/interfaces. Em Zig, sem herança, o padrão é implementado com ponteiros opacos (*anyopaque) e ponteiros de função, criando manualmente o equivalente a uma vtable.

Quando Usar

  • Coleções heterogêneas (diferentes tipos, mesma interface)
  • Callbacks com contexto de tipo desconhecido
  • Plugins e sistemas extensíveis em runtime
  • Quando comptime generics não são suficientes (tipo decidido em runtime)

A Técnica Fundamental

const std = @import("std");

// Interface "apagada" — não sabe o tipo concreto
const Writer = struct {
    ptr: *anyopaque,
    writeFn: *const fn (*anyopaque, []const u8) anyerror!usize,

    pub fn write(self: Writer, dados: []const u8) !usize {
        return self.writeFn(self.ptr, dados);
    }

    // Função helper para criar Writer a partir de qualquer tipo com write()
    pub fn init(ptr: anytype) Writer {
        const T = @TypeOf(ptr);
        const impl = struct {
            fn write(opaque: *anyopaque, dados: []const u8) anyerror!usize {
                const self: T = @ptrCast(@alignCast(opaque));
                return self.write(dados);
            }
        };
        return .{
            .ptr = @ptrCast(@alignCast(ptr)),
            .writeFn = impl.write,
        };
    }
};

// Tipo concreto 1
const FileWriter = struct {
    arquivo: std.fs.File,

    pub fn write(self: *FileWriter, dados: []const u8) !usize {
        return self.arquivo.write(dados);
    }

    pub fn writer(self: *FileWriter) Writer {
        return Writer.init(self);
    }
};

// Tipo concreto 2
const BufferWriter = struct {
    buffer: []u8,
    pos: usize = 0,

    pub fn write(self: *BufferWriter, dados: []const u8) !usize {
        const espaco = self.buffer.len - self.pos;
        const n = @min(dados.len, espaco);
        @memcpy(self.buffer[self.pos..][0..n], dados[0..n]);
        self.pos += n;
        return n;
    }

    pub fn writer(self: *BufferWriter) Writer {
        return Writer.init(self);
    }
};

// Código genérico que aceita qualquer "Writer"
fn escreverSaudacao(w: Writer) !void {
    _ = try w.write("Olá, mundo!\n");
}

pub fn main() !void {
    // Pode usar qualquer implementação
    var buffer: [100]u8 = undefined;
    var buf_writer = BufferWriter{ .buffer = &buffer };
    try escreverSaudacao(buf_writer.writer());

    std.debug.print("Buffer: {s}\n", .{buffer[0..buf_writer.pos]});
}

Type Erasure na std

A biblioteca padrão de Zig usa type erasure extensivamente:

const std = @import("std");

// std.mem.Allocator é type erasure!
// Internamente: ptr: *anyopaque + vtable de funções
const allocator: std.mem.Allocator = undefined;

// std.io.Writer também é type erasure genérica
// std.io.Writer(Context, ErrorSet, writeFn)

// std.io.AnyWriter é type-erased writer
fn aceitaQualquerWriter(writer: std.io.AnyWriter) !void {
    try writer.writeAll("funciona com qualquer implementação\n");
}

Interface Completa com Múltiplos Métodos

const std = @import("std");

const Forma = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    const VTable = struct {
        area: *const fn (*anyopaque) f64,
        perimetro: *const fn (*anyopaque) f64,
        nome: *const fn (*anyopaque) []const u8,
    };

    pub fn area(self: Forma) f64 {
        return self.vtable.area(self.ptr);
    }

    pub fn perimetro(self: Forma) f64 {
        return self.vtable.perimetro(self.ptr);
    }

    pub fn nome(self: Forma) []const u8 {
        return self.vtable.nome(self.ptr);
    }

    pub fn init(comptime T: type, ptr: *T) Forma {
        const impl = struct {
            fn area(opaque: *anyopaque) f64 {
                const self: *T = @ptrCast(@alignCast(opaque));
                return self.area();
            }
            fn perimetro(opaque: *anyopaque) f64 {
                const self: *T = @ptrCast(@alignCast(opaque));
                return self.perimetro();
            }
            fn nome(opaque: *anyopaque) []const u8 {
                const self: *T = @ptrCast(@alignCast(opaque));
                return self.nome();
            }
        };
        return .{
            .ptr = @ptrCast(ptr),
            .vtable = &.{
                .area = impl.area,
                .perimetro = impl.perimetro,
                .nome = impl.nome,
            },
        };
    }
};

const Circulo = struct {
    raio: f64,

    pub fn area(self: *Circulo) f64 {
        return std.math.pi * self.raio * self.raio;
    }
    pub fn perimetro(self: *Circulo) f64 {
        return 2.0 * std.math.pi * self.raio;
    }
    pub fn nome(_: *Circulo) []const u8 {
        return "Círculo";
    }
};

const Retangulo = struct {
    largura: f64,
    altura: f64,

    pub fn area(self: *Retangulo) f64 {
        return self.largura * self.altura;
    }
    pub fn perimetro(self: *Retangulo) f64 {
        return 2.0 * (self.largura + self.altura);
    }
    pub fn nome(_: *Retangulo) []const u8 {
        return "Retângulo";
    }
};

pub fn main() void {
    var circulo = Circulo{ .raio = 5.0 };
    var retangulo = Retangulo{ .largura = 4.0, .altura = 6.0 };

    // Coleção heterogênea de formas
    const formas = [_]Forma{
        Forma.init(Circulo, &circulo),
        Forma.init(Retangulo, &retangulo),
    };

    for (formas) |forma| {
        std.debug.print("{s}: área={d:.2}, perímetro={d:.2}\n", .{
            forma.nome(), forma.area(), forma.perimetro(),
        });
    }
}

Quando Evitar

  • Quando o tipo é conhecido em compilação — use comptime generics
  • Quando há apenas um ou dois tipos — tagged union é mais simples
  • Hot paths onde o overhead de indirection é significativo
  • Quando a coleção heterogênea não é necessária

Veja Também

Continue aprendendo Zig

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