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
- Comptime — Polimorfismo estático sem overhead
- Strategy — Algoritmos intercambiáveis
- Adapter — Adaptar interfaces
- Allocators — Exemplo real de type erasure
- Enums e Unions — Tagged unions como alternativa