Se existe uma feature que define o que torna Zig especial, é o comptime. Enquanto linguagens como C dependem de um preprocessador baseado em substituição de texto, e C++ usa templates com sintaxe críptica, Zig permite que você execute código real da linguagem em tempo de compilação — com a mesma sintaxe, as mesmas regras e a mesma depurabilidade do código que roda em tempo de execução.
Neste tutorial, vamos explorar comptime em profundidade: desde os conceitos básicos até técnicas avançadas de metaprogramação que farão você repensar o que é possível em uma linguagem de sistemas. Se você ainda está começando com Zig, recomendamos ler a Introdução ao Zig antes de continuar.
O que é Comptime?
comptime é uma palavra-chave do Zig que instrui o compilador a avaliar expressões durante a compilação em vez de em tempo de execução. Mas vai muito além de simples constantes — em Zig, você pode executar funções inteiras, iterar sobre arrays, manipular tipos e até gerar structs completas, tudo durante a compilação.
A Ideia Central
A filosofia do comptime é: não crie uma linguagem separada para metaprogramação. Em vez disso, use a mesma linguagem que você já conhece.
const std = @import("std");
fn fibonacci(n: u16) u16 {
if (n == 0 or n == 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
pub fn main() void {
// Calculado em TEMPO DE COMPILAÇÃO
// O binário já contém o valor 55 — zero custo em runtime
const fib10 = comptime fibonacci(10);
std.debug.print("fibonacci(10) = {}\n", .{fib10});
}
Observe: fibonacci é uma função normal. Não existe nenhuma anotação especial que a torna “compatível com comptime”. O compilador simplesmente executa a função durante a compilação quando você usa a palavra-chave comptime.
O que Comptime Pode Fazer
| Capacidade | Exemplo |
|---|---|
| Calcular valores constantes | comptime fibonacci(10) |
| Definir tamanhos de arrays | var arr: [comptime tamanho()]u8 = undefined; |
| Criar tipos genéricos | fn ArrayList(comptime T: type) type { ... } |
| Reflexão sobre tipos | @typeInfo(T) para inspecionar structs, enums, etc. |
| Gerar código condicionalmente | if (comptime builtin.os.tag == .linux) { ... } |
| Validar entradas em compilação | @compileError("mensagem") |
| Desdobrar loops | inline for sobre tuplas conhecidas em comptime |
O que Comptime NÃO Pode Fazer
Para manter a linguagem previsível, comptime tem restrições intencionais:
- Não pode fazer I/O: sem acesso a arquivos, rede ou stdin/stdout durante compilação.
- Não pode alocar memória em heap: sem
mallocou allocators em comptime. - Não depende da arquitetura do host: o código comptime não sabe em qual máquina está compilando (isso garante builds reproduzíveis).
- Não pode chamar funções externas C (como
printf) em comptime.
Comptime vs Macros C/C++
Programadores vindos de C frequentemente perguntam: “Comptime substitui macros?”. A resposta é sim — e é imensamente superior. Para uma comparação completa entre Zig e C, veja nosso tutorial dedicado.
Comparação Direta
Macro C — MAX genérico:
// Problemas:
// 1. Sem verificação de tipos
// 2. Efeitos colaterais (avaliação dupla de argumentos)
// 3. Impossível depurar com gdb/lldb
// 4. Mensagens de erro incompreensíveis
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5;
int resultado = MAX(x++, 3);
// BUG: x++ avaliado DUAS vezes! resultado imprevisível
Zig — Função comptime genérica:
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
pub fn main() void {
// Seguro, tipado, depurável
var x: i32 = 5;
const resultado = max(i32, x, 3);
// x avaliado UMA vez. Comportamento previsível.
// Erro de compilação se T não suportar '>'
// const errado = max([]const u8, "a", "b");
// ^^^ erro: operador '>' não definido para []const u8
}
Compilação Condicional
C:
#ifdef DEBUG
printf("debug: x = %d\n", x);
#endif
#ifdef _WIN32
#include <windows.h>
#elif __linux__
#include <unistd.h>
#endif
Zig:
const builtin = @import("builtin");
const std = @import("std");
pub fn main() void {
// Compilação condicional — mas é código REAL, não texto
if (builtin.mode == .Debug) {
std.debug.print("debug: x = {}\n", .{x});
}
// O compilador elimina esse bloco em ReleaseFast
// Detecção de plataforma
const os = builtin.os.tag;
if (os == .windows) {
// Código Windows
} else if (os == .linux) {
// Código Linux
}
}
Tabela Comparativa
| Aspecto | Macros C/C++ | Templates C++ | Comptime Zig |
|---|---|---|---|
| Linguagem | Substituição de texto | Meta-linguagem separada | Mesma linguagem |
| Verificação de tipos | ❌ Nenhuma | ✅ Parcial | ✅ Completa |
| Depuração | ❌ Impossível | ❌ Muito difícil | ✅ Normal |
| Mensagens de erro | ❌ Crípticas | ❌ Páginas de erros | ✅ Claras e localizadas |
| Efeitos colaterais | ❌ Possíveis | ✅ Sem | ✅ Sem |
| Recursão | ❌ Limitada | ✅ Sim | ✅ Sim |
| Geração de tipos | ❌ Não | ✅ Sim | ✅ Sim |
Variáveis e Blocos Comptime
Variáveis comptime
Uma variável comptime existe apenas durante a compilação e nunca aparece no binário final:
pub fn main() void {
// Variável comptime — calculada durante compilação
comptime var x: i32 = 0;
x += 1;
x += 1;
x += 1;
// x agora vale 3, mas esse cálculo aconteceu no compilador
// Atribuir a uma constante runtime
const resultado: i32 = x; // No binário, isso é simplesmente "3"
std.debug.print("resultado = {}\n", .{resultado});
}
Blocos comptime
Você pode marcar um bloco inteiro como comptime para executar lógica complexa durante a compilação:
const std = @import("std");
pub fn main() void {
// Bloco comptime com resultado
const mensagem = comptime blk: {
var buf: [64]u8 = undefined;
const texto = "ZIG";
var i: usize = 0;
for (texto) |c| {
buf[i] = c;
i += 1;
buf[i] = ' ';
i += 1;
}
break :blk buf[0..i];
};
std.debug.print("Mensagem: {s}\n", .{mensagem});
// Saída: "Mensagem: Z I G "
}
comptime_int e comptime_float
Literais numéricos em Zig são de tipos especiais que só existem em comptime:
const std = @import("std");
pub fn main() void {
// Literais são comptime_int — precisão arbitrária!
const grande = 100_000_000_000_000_000_000;
// ^^^ isso não caberia em u64, mas comptime_int suporta
// Ao atribuir a um tipo concreto, o compilador verifica o range
const x: u8 = 42; // OK: 42 cabe em u8
// const y: u8 = 256; // ERRO: 256 não cabe em u8
// comptime_float é f128 internamente
const pi = 3.14159265358979323846;
const pi_f32: f32 = pi; // Conversão com possível perda de precisão
const pi_f64: f64 = pi; // Mais precisão preservada
std.debug.print("pi f32 = {d:.15}\n", .{pi_f32});
std.debug.print("pi f64 = {d:.15}\n", .{pi_f64});
}
Funções com Parâmetros Comptime
Aqui as coisas ficam realmente interessantes. Quando um parâmetro de função é marcado como comptime, o valor deve ser conhecido em tempo de compilação. Isso é a base dos generics em Zig.
Generics Básicos
const std = @import("std");
// Função genérica que funciona com qualquer tipo numérico
fn somar(comptime T: type, a: T, b: T) T {
return a + b;
}
pub fn main() void {
// O compilador gera versões especializadas para cada tipo
const r1 = somar(i32, 10, 20); // Versão para i32
const r2 = somar(f64, 3.14, 2.71); // Versão para f64
const r3 = somar(u8, 100, 50); // Versão para u8
std.debug.print("i32: {}, f64: {d:.2}, u8: {}\n", .{ r1, r2, r3 });
}
Funções que Retornam Tipos
Em Zig, tipos são valores de primeira classe em comptime. Uma função pode receber e retornar tipos, incluindo structs, enums e unions:
const std = @import("std");
// Função que cria um tipo de array com tamanho customizado
fn Array(comptime T: type, comptime tamanho: comptime_int) type {
return [tamanho]T;
}
// Função que cria um tipo de par (tupla tipada)
fn Par(comptime A: type, comptime B: type) type {
return struct {
primeiro: A,
segundo: B,
const Self = @This();
pub fn criar(a: A, b: B) Self {
return .{ .primeiro = a, .segundo = b };
}
pub fn trocar(self: Self) Par(B, A) {
return Par(B, A).criar(self.segundo, self.primeiro);
}
};
}
pub fn main() void {
// Tipo Array de 5 inteiros
const MeuArray = Array(i32, 5);
var arr: MeuArray = .{ 1, 2, 3, 4, 5 };
arr[0] = 10;
// Tipo Par de string e inteiro
const par = Par([]const u8, i32).criar("idade", 30);
std.debug.print("{s} = {}\n", .{ par.primeiro, par.segundo });
// Trocar os elementos do par
const invertido = par.trocar();
std.debug.print("{} = {s}\n", .{ invertido.primeiro, invertido.segundo });
}
anytype — Inferência em Comptime
Quando um parâmetro é declarado como anytype, o compilador infere o tipo automaticamente. Isso funciona como “templates” implícitos:
const std = @import("std");
// anytype permite aceitar qualquer tipo
fn dobrar(valor: anytype) @TypeOf(valor) {
return valor + valor;
}
fn imprimir(valor: anytype) void {
const T = @TypeOf(valor);
switch (@typeInfo(T)) {
.int, .comptime_int => std.debug.print("Inteiro: {}\n", .{valor}),
.float, .comptime_float => std.debug.print("Float: {d:.4}\n", .{valor}),
.pointer => |ptr_info| {
if (ptr_info.size == .Slice and ptr_info.child == u8) {
std.debug.print("String: {s}\n", .{valor});
} else {
std.debug.print("Ponteiro: {*}\n", .{valor});
}
},
else => std.debug.print("Tipo: {}\n", .{@typeName(T)}),
}
}
pub fn main() void {
std.debug.print("{}\n", .{dobrar(@as(i32, 21))}); // 42
std.debug.print("{d}\n", .{dobrar(@as(f64, 1.5))}); // 3.0
imprimir(@as(i32, 42));
imprimir(@as(f64, 3.14));
imprimir("Olá Zig!");
}
Tipos como Valores de Primeira Classe
Em Zig, type é um tipo real que pode ser armazenado em variáveis, passado para funções e retornado — tudo em comptime. Isso é radicalmente diferente de qualquer outra linguagem de sistemas.
Manipulando Tipos
const std = @import("std");
pub fn main() void {
// Tipos podem ser armazenados em constantes
const MeuTipo = i32;
var x: MeuTipo = 42;
x += 1;
// Tipos podem ser escolhidos condicionalmente
const Numero = if (@sizeOf(usize) >= 8) i64 else i32;
const valor: Numero = 1000;
std.debug.print("Tamanho de Numero: {} bytes\n", .{@sizeOf(Numero)});
std.debug.print("Valor: {}\n", .{valor});
// Branching sobre tipos em comptime
const tipo_escolhido = comptime blk: {
const arquitetura = @import("builtin").cpu.arch;
break :blk switch (arquitetura) {
.x86_64 => f64,
.aarch64 => f64,
else => f32,
};
};
std.debug.print("Tipo de float para esta arquitetura: {}\n", .{@typeName(tipo_escolhido)});
}
Criando Tipos Dinamicamente com @Type
A builtin @Type permite construir tipos programaticamente a partir de uma descrição:
const std = @import("std");
// Criar um tipo inteiro com N+1 bits
fn InteiroMaior(comptime T: type) type {
const info = @typeInfo(T).int;
return @Type(.{
.int = .{
.bits = info.bits + 1,
.signedness = info.signedness,
},
});
}
// Criar um tipo inteiro sem sinal a partir de um com sinal
fn SemSinal(comptime T: type) type {
const info = @typeInfo(T).int;
return @Type(.{
.int = .{
.bits = info.bits,
.signedness = .unsigned,
},
});
}
test "manipulação de tipos" {
// u8 → u9
try std.testing.expect(InteiroMaior(u8) == u9);
// i32 → i33
try std.testing.expect(InteiroMaior(i32) == i33);
// i64 → u64
try std.testing.expect(SemSinal(i64) == u64);
// i16 → u16
try std.testing.expect(SemSinal(i16) == u16);
}
Geração de Código em Comptime
Uma das aplicações mais poderosas de comptime é a geração de dados e código durante a compilação — sem nenhum custo em tempo de execução.
Lookup Tables
const std = @import("std");
// Gerar tabela de quadrados em comptime
fn gerarTabelaQuadrados(comptime tamanho: usize) [tamanho]u64 {
var tabela: [tamanho]u64 = undefined;
for (0..tamanho) |i| {
tabela[i] = i * i;
}
return tabela;
}
// Gerar tabela de senos pré-calculados (para games/DSP)
fn gerarTabelaSenos(comptime pontos: usize) [pontos]f64 {
var tabela: [pontos]f64 = undefined;
const passo = 2.0 * std.math.pi / @as(f64, @floatFromInt(pontos));
for (0..pontos) |i| {
tabela[i] = @sin(passo * @as(f64, @floatFromInt(i)));
}
return tabela;
}
// CRC32 lookup table — usada em checksum de dados
fn gerarCRC32Table() [256]u32 {
var tabela: [256]u32 = undefined;
for (0..256) |i| {
var crc: u32 = @intCast(i);
for (0..8) |_| {
if (crc & 1 == 1) {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc = crc >> 1;
}
}
tabela[i] = crc;
}
return tabela;
}
// Todas estas tabelas são calculadas em TEMPO DE COMPILAÇÃO
// e incorporadas ao binário como dados estáticos
const QUADRADOS = comptime gerarTabelaQuadrados(100);
const SENOS = comptime gerarTabelaSenos(360);
const CRC32_TABLE = comptime gerarCRC32Table();
pub fn main() void {
std.debug.print("7² = {}\n", .{QUADRADOS[7]}); // 49
std.debug.print("sin(90°) ≈ {d:.6}\n", .{SENOS[90]}); // ~1.0
std.debug.print("CRC32[0] = 0x{X:0>8}\n", .{CRC32_TABLE[0]}); // 0x00000000
std.debug.print("CRC32[1] = 0x{X:0>8}\n", .{CRC32_TABLE[1]}); // 0x77073096
}
💡 Por que isso importa? Em C, para gerar essas tabelas em tempo de compilação, você precisaria de scripts Python/Perl gerando código C, ou macros absurdamente complexas. Em Zig, é apenas uma função normal com a palavra-chave
comptime.
Implementações Condicionais
Use comptime para selecionar a melhor implementação com base nas características da plataforma (veja também cross-compilation em Zig):
const std = @import("std");
const builtin = @import("builtin");
fn somaOtimizada(slice: []const i32) i64 {
// Se a CPU suporta SIMD, use instruções vetoriais
if (comptime std.Target.x86.featureSetHas(builtin.cpu.features, .avx2)) {
return somaAVX2(slice);
} else if (comptime std.Target.x86.featureSetHas(builtin.cpu.features, .sse2)) {
return somaSSE2(slice);
} else {
return somaEscalar(slice);
}
}
fn somaEscalar(slice: []const i32) i64 {
var total: i64 = 0;
for (slice) |valor| {
total += valor;
}
return total;
}
fn somaSSE2(slice: []const i32) i64 {
// Implementação SSE2 aqui
return somaEscalar(slice); // fallback simplificado
}
fn somaAVX2(slice: []const i32) i64 {
// Implementação AVX2 aqui
return somaEscalar(slice); // fallback simplificado
}
O compilador remove completamente os branches que não se aplicam à plataforma alvo. O binário final contém apenas a implementação escolhida.
@typeInfo e Reflexão em Comptime
@typeInfo é a ferramenta de reflexão do Zig. Ela retorna uma tagged union que descreve completamente qualquer tipo. Combinada com comptime, permite criar código que se adapta automaticamente a qualquer tipo de dado.
Inspecionando Tipos
const std = @import("std");
fn descreverTipo(comptime T: type) void {
const info = @typeInfo(T);
switch (info) {
.int => |i| {
const sinal = if (i.signedness == .signed) "com sinal" else "sem sinal";
@compileLog("Inteiro " ++ sinal, i.bits, "bits");
},
.float => |f| {
@compileLog("Float de", f.bits, "bits");
},
.@"struct" => |s| {
@compileLog("Struct com", s.fields.len, "campos");
for (s.fields) |campo| {
@compileLog(" campo:", campo.name);
}
},
.pointer => |p| {
@compileLog("Ponteiro para", @typeName(p.child));
},
else => @compileLog("Outro tipo:", @typeName(T)),
}
}
const Pessoa = struct {
nome: []const u8,
idade: u32,
ativo: bool,
};
// Chamado em comptime — imprime informações no log de compilação
comptime {
descreverTipo(i32); // "Inteiro com sinal, 32 bits"
descreverTipo(f64); // "Float de 64 bits"
descreverTipo(Pessoa); // "Struct com 3 campos"
descreverTipo(*const u8); // "Ponteiro para u8"
}
Iterando sobre Campos de uma Struct
Este é um padrão extremamente útil — percorrer todos os campos de uma struct em comptime:
const std = @import("std");
const Configuracao = struct {
host: []const u8 = "localhost",
porta: u16 = 8080,
max_conexoes: u32 = 100,
debug: bool = false,
timeout_ms: u64 = 5000,
};
fn imprimirCampos(comptime T: type, valor: T) void {
const info = @typeInfo(T).@"struct";
std.debug.print("=== {} ===\n", .{@typeName(T)});
inline for (info.fields) |campo| {
const v = @field(valor, campo.name);
std.debug.print(" {s}: {any}\n", .{ campo.name, v });
}
}
pub fn main() void {
const config = Configuracao{
.host = "meuservidor.com",
.porta = 3000,
};
imprimirCampos(Configuracao, config);
}
Saída:
=== Configuracao ===
host: meuservidor.com
porta: 3000
max_conexoes: 100
debug: false
timeout_ms: 5000
Serialização Automática para JSON
Combinando reflexão com comptime, podemos criar um serializador genérico:
const std = @import("std");
fn paraJSON(comptime T: type, valor: T, writer: anytype) !void {
const info = @typeInfo(T);
switch (info) {
.@"struct" => |s| {
try writer.writeAll("{");
var primeiro = true;
inline for (s.fields) |campo| {
if (!primeiro) try writer.writeAll(",");
primeiro = false;
try writer.print("\"{s}\":", .{campo.name});
try paraJSON(campo.type, @field(valor, campo.name), writer);
}
try writer.writeAll("}");
},
.int, .comptime_int => try writer.print("{}", .{valor}),
.float, .comptime_float => try writer.print("{d}", .{valor}),
.bool => try writer.print("{}", .{valor}),
.pointer => |ptr| {
if (ptr.size == .Slice and ptr.child == u8) {
try writer.print("\"{s}\"", .{valor});
}
},
.optional => {
if (valor) |v| {
try paraJSON(@typeInfo(T).optional.child, v, writer);
} else {
try writer.writeAll("null");
}
},
else => try writer.writeAll("null"),
}
}
const Produto = struct {
nome: []const u8,
preco: f64,
estoque: u32,
disponivel: bool,
};
pub fn main() !void {
const produto = Produto{
.nome = "Teclado Mecânico",
.preco = 299.90,
.estoque = 42,
.disponivel = true,
};
const stdout = std.io.getStdOut().writer();
try paraJSON(Produto, produto, stdout);
try stdout.writeAll("\n");
}
Saída:
{"nome":"Teclado Mecânico","preco":299.9,"estoque":42,"disponivel":true}
O mais impressionante: o compilador conhece todos os campos em tempo de compilação, então o código gerado é tão eficiente quanto escrever a serialização na mão para cada tipo.
Exemplos Práticos
Exemplo 1: ArrayList Genérico
Vamos implementar uma versão simplificada de ArrayList — a estrutura de dados mais usada em Zig. Este exemplo também demonstra o uso de allocators:
const std = @import("std");
const Allocator = std.mem.Allocator;
fn ArrayList(comptime T: type) type {
return struct {
items: []T,
capacidade: usize,
tamanho: usize,
allocator: Allocator,
const Self = @This();
pub fn init(allocator: Allocator) Self {
return .{
.items = &[_]T{},
.capacidade = 0,
.tamanho = 0,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
if (self.capacidade > 0) {
self.allocator.free(self.items.ptr[0..self.capacidade]);
}
}
pub fn append(self: *Self, item: T) !void {
if (self.tamanho >= self.capacidade) {
try self.crescer();
}
self.items.ptr[self.tamanho] = item;
self.tamanho += 1;
self.items.len = self.tamanho;
}
pub fn get(self: Self, indice: usize) T {
if (indice >= self.tamanho) {
@panic("índice fora dos limites");
}
return self.items[indice];
}
pub fn slice(self: Self) []const T {
return self.items.ptr[0..self.tamanho];
}
fn crescer(self: *Self) !void {
const nova_capacidade = if (self.capacidade == 0)
8
else
self.capacidade * 2;
const novo_buf = try self.allocator.alloc(T, nova_capacidade);
if (self.tamanho > 0) {
@memcpy(novo_buf[0..self.tamanho], self.items.ptr[0..self.tamanho]);
}
if (self.capacidade > 0) {
self.allocator.free(self.items.ptr[0..self.capacidade]);
}
self.items.ptr = novo_buf.ptr;
self.items.len = self.tamanho;
self.capacidade = nova_capacidade;
}
// Informações de tipo disponíveis em comptime
pub const Item = T;
pub const tamanho_item = @sizeOf(T);
};
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// ArrayList de inteiros
var numeros = ArrayList(i32).init(allocator);
defer numeros.deinit();
try numeros.append(10);
try numeros.append(20);
try numeros.append(30);
for (numeros.slice()) |n| {
std.debug.print("{} ", .{n});
}
std.debug.print("\n", .{});
// ArrayList de strings
var nomes = ArrayList([]const u8).init(allocator);
defer nomes.deinit();
try nomes.append("Ana");
try nomes.append("Bruno");
try nomes.append("Carlos");
for (nomes.slice()) |nome| {
std.debug.print("{s} ", .{nome});
}
std.debug.print("\n", .{});
// Meta-informação disponível em comptime
std.debug.print("Tamanho de cada item (i32): {} bytes\n", .{ArrayList(i32).tamanho_item});
std.debug.print("Tamanho de cada item (f64): {} bytes\n", .{ArrayList(f64).tamanho_item});
}
Exemplo 2: Formatador Customizado com comptime
Implementando format para suas structs, você integra com todo o sistema de formatação do Zig:
const std = @import("std");
const Vetor3D = struct {
x: f64,
y: f64,
z: f64,
const Self = @This();
// O formato do comptime fmt permite customizar a saída
pub fn format(
self: Self,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = options;
if (comptime std.mem.eql(u8, fmt, "coords")) {
// Formato de coordenadas: (x, y, z)
try writer.print("({d:.2}, {d:.2}, {d:.2})", .{ self.x, self.y, self.z });
} else if (comptime std.mem.eql(u8, fmt, "mag")) {
// Formato de magnitude
const mag = @sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
try writer.print("|v| = {d:.4}", .{mag});
} else if (fmt.len == 0) {
// Formato padrão
try writer.print("Vec3({d:.2}, {d:.2}, {d:.2})", .{ self.x, self.y, self.z });
} else {
// Formato não reconhecido — erro de COMPILAÇÃO
std.fmt.invalidFmtError(fmt, self);
}
}
pub fn soma(self: Self, outro: Self) Self {
return .{
.x = self.x + outro.x,
.y = self.y + outro.y,
.z = self.z + outro.z,
};
}
};
pub fn main() void {
const v = Vetor3D{ .x = 1.0, .y = 2.0, .z = 3.0 };
std.debug.print("Padrão: {}\n", .{v});
std.debug.print("Coordenadas: {coords}\n", .{v});
std.debug.print("Magnitude: {mag}\n", .{v});
// O compilador valida o formato em TEMPO DE COMPILAÇÃO
// std.debug.print("{invalido}\n", .{v}); // Erro de compilação!
}
Saída:
Padrão: Vec3(1.00, 2.00, 3.00)
Coordenadas: (1.00, 2.00, 3.00)
Magnitude: |v| = 3.7417
Exemplo 3: Gerador de HashMap Perfeito em Comptime
Quando as chaves são conhecidas em tempo de compilação, podemos gerar um mapa otimizado:
const std = @import("std");
fn MapaEstatico(
comptime chaves: []const []const u8,
comptime V: type,
) type {
return struct {
valores: [chaves.len]V,
const Self = @This();
// Busca O(n) mas com N pequeno e inline, é mais rápido
// que hash map para poucos elementos
pub fn get(self: Self, chave: []const u8) ?V {
inline for (chaves, 0..) |k, i| {
if (std.mem.eql(u8, k, chave)) {
return self.valores[i];
}
}
return null;
}
pub fn init(valores: [chaves.len]V) Self {
return .{ .valores = valores };
}
// Validação em comptime: chaves duplicadas
comptime {
for (chaves, 0..) |a, i| {
for (chaves[i + 1 ..]) |b| {
if (std.mem.eql(u8, a, b)) {
@compileError("Chaves duplicadas no mapa: " ++ a);
}
}
}
}
};
}
pub fn main() void {
// Mapa de HTTP status codes
const StatusMap = MapaEstatico(
&.{ "200", "201", "301", "400", "404", "500" },
[]const u8,
);
const status = StatusMap.init(.{
"OK",
"Created",
"Moved Permanently",
"Bad Request",
"Not Found",
"Internal Server Error",
});
// Busca é otimizada pelo compilador
if (status.get("404")) |desc| {
std.debug.print("404 = {s}\n", .{desc});
}
if (status.get("200")) |desc| {
std.debug.print("200 = {s}\n", .{desc});
}
if (status.get("999")) |desc| {
std.debug.print("999 = {s}\n", .{desc});
} else {
std.debug.print("999 = não encontrado\n", .{});
}
}
inline for e Desdobramento de Loops
O inline for é essencial quando você precisa iterar sobre dados conhecidos em comptime enquanto o corpo do loop contém operações runtime:
const std = @import("std");
// Sistema de validação genérico
fn Validador(comptime T: type) type {
return struct {
const Regra = struct {
nome: []const u8,
validar: *const fn (T) bool,
};
pub fn validarTudo(
valor: T,
comptime regras: []const Regra,
) !void {
inline for (regras) |regra| {
if (!regra.validar(valor)) {
std.debug.print("❌ Falhou: {s}\n", .{regra.nome});
return error.ValidacaoFalhou;
}
std.debug.print("✅ Passou: {s}\n", .{regra.nome});
}
}
};
}
fn ehPositivo(n: i32) bool {
return n > 0;
}
fn ehPar(n: i32) bool {
return @mod(n, 2) == 0;
}
fn menorQue100(n: i32) bool {
return n < 100;
}
pub fn main() !void {
const V = Validador(i32);
const regras = [_]V.Regra{
.{ .nome = "positivo", .validar = &ehPositivo },
.{ .nome = "par", .validar = &ehPar },
.{ .nome = "menor que 100", .validar = &menorQue100 },
};
std.debug.print("Validando 42:\n", .{});
try V.validarTudo(42, ®ras);
std.debug.print("\nValidando -5:\n", .{});
V.validarTudo(-5, ®ras) catch {
std.debug.print("Validação falhou (esperado)\n", .{});
};
}
Boas Práticas e Armadilhas Comuns
✅ Boas Práticas
1. Use comptime para eliminar overhead de abstração:
// BOM: genérico sem custo em runtime
fn comparar(comptime T: type, a: T, b: T) std.math.Order {
return std.math.order(a, b);
}
2. Valide entradas em comptime com @compileError (veja também testes em Zig para validação em runtime):
fn criarBuffer(comptime tamanho: usize) [tamanho]u8 {
if (tamanho == 0) {
@compileError("Buffer não pode ter tamanho zero");
}
if (tamanho > 1024 * 1024) {
@compileError("Buffer na stack não deve exceder 1MB");
}
return undefined;
}
3. Use inline for apenas quando necessário:
// inline for é necessário quando o corpo depende do tipo do campo
fn somarCamposNumericos(comptime T: type, valor: T) f64 {
var soma: f64 = 0;
inline for (@typeInfo(T).@"struct".fields) |campo| {
if (@typeInfo(campo.type) == .int or @typeInfo(campo.type) == .float) {
soma += @as(f64, @floatFromInt(@field(valor, campo.name)));
}
}
return soma;
}
4. Nomeie funções que retornam tipos com PascalCase:
// Convenção do Zig: funções que retornam type usam PascalCase
fn HashMap(comptime K: type, comptime V: type) type { ... }
fn ArrayList(comptime T: type) type { ... }
// Funções normais usam camelCase
fn calcularTotal(items: []const Item) u64 { ... }
❌ Armadilhas Comuns
1. Não confunda comptime var com const:
pub fn main() void {
// ATENÇÃO: comptime var é avaliada pelo compilador
comptime var x: i32 = 0;
x += 1; // Isso roda no COMPILADOR, não em runtime
// Para variáveis runtime, use var normalmente
var y: i32 = 0;
y += 1; // Isso roda em RUNTIME
}
2. Cuidado com o tamanho do binário:
// CUIDADO: inline for com muitos elementos gera muito código
fn processar(dados: [1000]u32) void {
// Isso gera 1000 cópias do corpo do loop!
inline for (dados) |d| {
fazAlgo(d);
}
// MELHOR: use for normal quando inline não é necessário
for (dados) |d| {
fazAlgo(d);
}
}
3. Não force comptime desnecessariamente:
// RUIM: forçar comptime quando não há benefício
fn somar(a: i32, b: i32) i32 {
return comptime a + b; // ERRO: a e b não são conhecidos em comptime
}
// BOM: use comptime onde o valor PODE ser conhecido em compilação
fn criarArray(comptime tamanho: usize) [tamanho]u8 {
return [_]u8{0} ** tamanho;
}
4. Entenda os limites de memória do comptime:
// CUIDADO: o compilador tem limites de memória para avaliação comptime
fn gerarGrande() [1_000_000]u64 {
// Pode ser lento ou falhar durante compilação
// Tabelas muito grandes devem ser geradas externamente
// e embedadas com @embedFile
var tabela: [1_000_000]u64 = undefined;
for (0..1_000_000) |i| {
tabela[i] = i * i;
}
return tabela;
}
Dica Final: @compileLog para Depuração
@compileLog é o printf do comptime. Use para inspecionar valores durante a compilação:
fn meuGenerico(comptime T: type) type {
// Imprime no terminal DURANTE a compilação
@compileLog("Criando tipo para:", @typeName(T));
@compileLog("Tamanho:", @sizeOf(T), "bytes");
@compileLog("Alinhamento:", @alignOf(T), "bytes");
return struct {
dados: T,
};
}
⚠️ Nota:
@compileLogcausa um erro de compilação proposital — remova antes de fazer o build final. É apenas uma ferramenta de depuração.
Resumo: O que Você Aprendeu
| Conceito | O que faz | Quando usar |
|---|---|---|
comptime keyword | Força avaliação em tempo de compilação | Cálculos de constantes, tamanhos de array |
Parâmetros comptime | Permite generics | Funções que operam sobre tipos |
type como valor | Tipos são valores de primeira classe | Criar estruturas de dados genéricas |
@typeInfo | Reflexão sobre tipos | Serialização, validação, geração de código |
@Type | Criar tipos programaticamente | Manipulação avançada de tipos |
inline for | Desdobra loops em comptime | Iterar sobre campos de structs |
@compileError | Gerar erros em compilação | Validar parâmetros comptime |
@compileLog | Debug print em compilação | Depuração de código comptime |
Próximos Passos
Agora que você domina comptime, está pronto para ir mais fundo:
- 📖 Está começando com Zig? — Comece pelo Guia de Instalação para configurar seu ambiente.
- 🔄 Vem do C? — Veja como comptime substitui macros no nosso Guia de Migração C para Zig.
- 📚 Documentação oficial — A seção sobre comptime no Language Reference é a referência canônica.
- 🧪 Pratique — Reescreva uma macro C complexa que você usa como uma função comptime em Zig.
- 🏗️ Zig Build System — Veja como comptime é usado no sistema de build do Zig.
- 🔍 Estude a std — O código-fonte da biblioteca padrão do Zig é um masterclass em uso de comptime.
Este tutorial é parte da série avançada do ZigLang Brasil. Se comptime mudou sua forma de pensar sobre metaprogramação, compartilhe com outros desenvolvedores!