Vtable Interface em Zig — O que é e Como Usar
Definição
Uma vtable (virtual table) em Zig é uma struct de ponteiros de função usada para implementar polimorfismo em runtime. Como Zig não tem herança, traits ou interfaces na linguagem, vtables são o padrão idiomático para quando você precisa de dispatch dinâmico — ou seja, chamar métodos diferentes dependendo do tipo concreto, sem conhecer o tipo em tempo de compilação.
A biblioteca padrão do Zig usa vtables extensivamente. O tipo std.mem.Allocator é o exemplo mais conhecido: ele armazena um ponteiro para implementação e uma vtable com as funções alloc, resize e free.
Por que Vtable Interface Importa
- Polimorfismo em runtime: Armazenar diferentes tipos em uma mesma variável.
- Extensibilidade: Novos tipos podem implementar a interface sem modificar código existente.
- Padrão da std lib: Allocator, Writer e Reader da std usam vtables.
- Controle total: O programador controla o layout de memória e o overhead.
Exemplo Prático
Interface Forma com Vtable
const std = @import("std");
const Forma = struct {
ptr: *anyopaque,
vtable: *const VTable,
const VTable = struct {
areaFn: *const fn (ptr: *anyopaque) f64,
nomeFn: *const fn (ptr: *anyopaque) []const u8,
};
pub fn area(self: Forma) f64 {
return self.vtable.areaFn(self.ptr);
}
pub fn nome(self: Forma) []const u8 {
return self.vtable.nomeFn(self.ptr);
}
pub fn init(ptr: anytype) Forma {
const T = @TypeOf(ptr);
const PtrInfo = @typeInfo(T);
const Child = PtrInfo.pointer.child;
return .{
.ptr = @ptrCast(ptr),
.vtable = &.{
.areaFn = @ptrCast(&struct {
pub fn call(p: *anyopaque) f64 {
const self: *Child = @ptrCast(@alignCast(p));
return self.area();
}
}.call),
.nomeFn = @ptrCast(&struct {
pub fn call(p: *anyopaque) []const u8 {
const self: *Child = @ptrCast(@alignCast(p));
return self.nome();
}
}.call),
},
};
}
};
Implementando a Interface
const std = @import("std");
const Circulo = struct {
raio: f64,
pub fn area(self: *Circulo) f64 {
return std.math.pi * self.raio * 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 nome(_: *Retangulo) []const u8 {
return "Retângulo";
}
};
pub fn main() void {
var circ = Circulo{ .raio = 5.0 };
var ret = Retangulo{ .largura = 4.0, .altura = 6.0 };
// Ambos podem ser tratados como Forma
const formas = [_]Forma{
Forma.init(&circ),
Forma.init(&ret),
};
for (formas) |forma| {
std.debug.print("{s}: área = {d:.2}\n", .{ forma.nome(), forma.area() });
}
}
Padrão da std lib: Allocator
const std = @import("std");
// std.mem.Allocator é uma vtable interface!
// Qualquer allocator pode ser usado uniformemente:
fn alocarEUsar(allocator: std.mem.Allocator) !void {
const dados = try allocator.alloc(u8, 100);
defer allocator.free(dados);
@memset(dados, 'Z');
std.debug.print("Primeiros bytes: {s}\n", .{dados[0..5]});
}
pub fn main() !void {
// Mesma função, diferentes implementações
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
try alocarEUsar(gpa.allocator()); // GPA
try alocarEUsar(std.heap.page_allocator); // Page allocator
}
Quando Usar Vtable vs anytype
| Critério | anytype (comptime) | Vtable (runtime) |
|---|---|---|
| Tipo conhecido em comptime | Sim | Nunca |
| Armazenar em coleção | Apenas mesmo tipo | Tipos diferentes |
| Performance | Inlining possível | Indireção por ponteiro |
| Tamanho do binário | Maior (duplicação) | Menor |
| Uso típico | Funções genéricas | Plugins, coleções heterogêneas |
Alternativa: Tagged Union
Para um conjunto fechado de tipos conhecidos em tempo de compilação, um union(enum) pode ser mais simples e eficiente que uma vtable:
const Forma = union(enum) {
circulo: struct { raio: f64 },
retangulo: struct { largura: f64, altura: f64 },
pub fn area(self: Forma) f64 {
return switch (self) {
.circulo => |c| std.math.pi * c.raio * c.raio,
.retangulo => |r| r.largura * r.altura,
};
}
};
Use vtable quando: novos tipos podem ser adicionados pelo usuário sem modificar a biblioteca (plugins, extensões, allocators). Use tagged union quando: o conjunto de tipos é fixo e conhecido na compilação.
Boas Práticas
- Imite o padrão
Allocatorda std: A convençãoptr: *anyopaque+vtable: *const VTableé bem estabelecida. Seguir esse padrão torna o código reconhecível para programadores Zig. - Mantenha a vtable em memória estática: Vtables geralmente são
conste vivem no segmento de dados. Declare-as comconst vtable = VTable{ ... }em escopo de namespace, não como variável local. - Documente os invariantes de lifetime: O ponteiro
ptrnão gerencia a vida do objeto concreto. Documente que o chamador é responsável por garantir que o objeto viva enquanto a interface for usada. - Considere
union(enum)antes de vtable: Vtables têm overhead de indireção e código mais verboso. Use-as apenas quando o polimorfismo em runtime com tipos abertos for realmente necessário.
Armadilhas Comuns
- Lifetime: O ponteiro na vtable deve apontar para dados que sobrevivem ao uso da interface. Cuidado com variáveis locais.
- Alinhamento:
@ptrCaste@alignCastsão necessários ao converter*anyopaquede volta para o tipo concreto. - Overhead: Cada chamada via vtable tem overhead de indireção. Para código hot-path, prefira
anytype(comptime). - Complexidade: Vtables manuais são verbosas. Avalie se
anytypeou umunion(enum)seriam mais simples.
Termos Relacionados
- anytype — Alternativa comptime a vtables
- Allocator — Exemplo clássico de vtable em Zig
- Tagged Union — Alternativa com tipos fechados
- Writer Interface — Interface que usa vtable internamente
- Struct — Structs armazenam a vtable