Se você já programou em C, provavelmente já sofreu com macros do preprocessador. Se veio do C++, sabe que templates podem se tornar um pesadelo de mensagens de erro. E se usou Rust, conhece o poder — e a complexidade — de proc macros. A linguagem Zig resolve esses problemas com uma abordagem única: o comptime.
O comptime (abreviação de compile-time) é o mecanismo que permite executar código Zig arbitrário durante a compilação. Não é uma linguagem separada, não é um preprocessador, não é um sistema de templates. É o mesmo Zig que você já conhece, rodando antes do programa estar pronto.
O que é Comptime?
Em Zig, a palavra-chave comptime marca expressões, variáveis e parâmetros que devem ser resolvidos em tempo de compilação. Diferente de constexpr em C++ ou const fn em Rust, o comptime de Zig suporta loops, condicionais, alocação de memória estática e até manipulação de tipos — tudo no mesmo código que você escreveria para runtime.
const std = @import("std");
fn fibonacci(comptime n: u32) u32 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// O resultado é calculado na compilação, não na execução
const fib_10 = fibonacci(10); // = 55
Nesse exemplo, fibonacci(10) é computado inteiramente pelo compilador. O binário final contém apenas o valor 55 — sem nenhum cálculo em runtime.
Comptime vs Runtime: Quando Usar Cada Um
A distinção entre comptime e runtime é fundamental em Zig. Variáveis comptime não existem no binário final — elas são resolvidas e substituídas durante a compilação.
fn processarDados(comptime T: type, dados: []const T) T {
// T é resolvido em comptime — o compilador gera código
// específico para cada tipo usado
var soma: T = 0;
for (dados) |valor| {
soma += valor;
}
return soma;
}
test "soma genérica" {
const inteiros = [_]i32{ 1, 2, 3, 4, 5 };
const floats = [_]f64{ 1.5, 2.5, 3.5 };
try std.testing.expectEqual(@as(i32, 15), processarDados(i32, &inteiros));
try std.testing.expectEqual(@as(f64, 7.5), processarDados(f64, &floats));
}
O compilador gera duas versões de processarDados — uma para i32 e outra para f64. Isso é semelhante à monomorphization de Rust, mas com uma sintaxe muito mais direta. Para entender melhor os tipos em Zig, confira o glossário sobre anytype.
Funções Comptime: O Coração da Metaprogramação
Funções em Zig podem receber parâmetros comptime e retornar tipos. Isso permite criar abstrações poderosas sem sacrificar performance ou legibilidade.
Geração de Tipos em Tempo de Compilação
Um dos usos mais poderosos do comptime é criar novos tipos dinamicamente:
fn VetorFixo(comptime T: type, comptime tamanho: usize) type {
return struct {
dados: [tamanho]T,
len: usize,
const Self = @This();
pub fn init() Self {
return .{
.dados = undefined,
.len = 0,
};
}
pub fn adicionar(self: *Self, valor: T) !void {
if (self.len >= tamanho) return error.Cheio;
self.dados[self.len] = valor;
self.len += 1;
}
pub fn obter(self: *const Self, indice: usize) ?T {
if (indice >= self.len) return null;
return self.dados[indice];
}
};
}
test "vetor fixo" {
var vec = VetorFixo(u32, 10).init();
try vec.adicionar(42);
try vec.adicionar(99);
try std.testing.expectEqual(@as(?u32, 42), vec.obter(0));
}
A função VetorFixo retorna um tipo. Ela é executada em tempo de compilação e gera uma struct completa com métodos. VetorFixo(u32, 10) e VetorFixo(f64, 100) são tipos completamente diferentes, otimizados individualmente pelo compilador.
Validação em Tempo de Compilação
O comptime permite validar restrições antes do programa sequer rodar:
fn MatrizQuadrada(comptime N: usize) type {
if (N == 0) @compileError("Matriz não pode ter dimensão zero");
if (N > 1024) @compileError("Dimensão máxima é 1024");
return struct {
dados: [N][N]f64,
pub fn identidade() @This() {
var m: @This() = .{ .dados = std.mem.zeroes([N][N]f64) };
for (0..N) |i| {
m.dados[i][i] = 1.0;
}
return m;
}
};
}
// Isso compila normalmente:
const Mat4 = MatrizQuadrada(4);
// Isso gera erro de compilação com mensagem clara:
// const MatInvalida = MatrizQuadrada(0);
// error: Matriz não pode ter dimensão zero
Compare isso com um erro de template C++ — em Zig, a mensagem de erro é exatamente o que você escreveu com @compileError. Sem pilhas de instanciação incompreensíveis.
Tabelas de Lookup em Tempo de Compilação
Construir tabelas pré-computadas é uma técnica clássica de otimização. Com comptime, isso é trivial:
fn gerarTabelaSeno(comptime tamanho: usize) [tamanho]f64 {
var tabela: [tamanho]f64 = undefined;
for (0..tamanho) |i| {
const angulo = @as(f64, @floatFromInt(i)) * (2.0 * std.math.pi) / @as(f64, @floatFromInt(tamanho));
tabela[i] = @sin(angulo);
}
return tabela;
}
// Tabela de 256 valores de seno, computada em compilação
const tabela_seno = comptime gerarTabelaSeno(256);
pub fn senoRapido(indice: u8) f64 {
return tabela_seno[indice];
}
A tabela_seno é calculada inteiramente pelo compilador e embutida no binário como dados estáticos. Em runtime, consultar um valor é apenas um acesso a array — O(1), sem nenhum cálculo trigonométrico.
Estruturas de Dados Genéricas com Comptime
O comptime é a base para generics em Zig. A biblioteca padrão usa esse padrão extensivamente. Veja como criar um hashmap genérico simplificado:
fn MapaSimples(comptime K: type, comptime V: type, comptime capacidade: usize) type {
return struct {
chaves: [capacidade]?K,
valores: [capacidade]V,
const Self = @This();
pub fn init() Self {
return .{
.chaves = [_]?K{null} ** capacidade,
.valores = undefined,
};
}
pub fn inserir(self: *Self, chave: K, valor: V) !void {
const indice = @as(usize, @intCast(chave)) % capacidade;
self.chaves[indice] = chave;
self.valores[indice] = valor;
}
pub fn buscar(self: *const Self, chave: K) ?V {
const indice = @as(usize, @intCast(chave)) % capacidade;
if (self.chaves[indice]) |k| {
if (k == chave) return self.valores[indice];
}
return null;
}
};
}
Para exemplos mais detalhados de uso de inline com loops em comptime, confira os cheatsheets de comptime.
Comptime vs C Macros: Por que Zig É Superior
O preprocessador C opera em texto — ele faz substituição textual sem entender a semântica do código. Isso causa problemas clássicos:
| Aspecto | Macros C | Comptime Zig |
|---|---|---|
| Verificação de tipos | Nenhuma | Completa |
| Debuggabilidade | Terrível | Normal |
| Mensagens de erro | Confusas | Claras com @compileError |
| Escopo | Global | Lexical normal |
| Composição | Frágil | Natural |
| Geração de tipos | Impossível | Nativa |
Em C, #define MAX(a, b) ((a) > (b) ? (a) : (b)) avalia argumentos duas vezes e não tem verificação de tipos. Em Zig:
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
Simples, seguro e com tipos verificados. Para uma comparação mais ampla entre Zig e C++, veja nosso artigo Zig vs C++.
Comptime vs Rust Proc Macros
Rust possui um sistema de macros poderoso, mas em camadas: macro_rules! para macros declarativas e proc macros para transformações de AST. Proc macros são essencialmente programas Rust separados que manipulam tokens.
O comptime de Zig é fundamentalmente diferente:
- Rust: proc macros são compiladores dentro de compiladores — uma crate separada, API diferente, debugging complexo
- Zig: comptime é o mesmo código, a mesma linguagem, as mesmas ferramentas
Quando um derive(Serialize) em Rust falha, o rastreamento de erros passa por camadas de geração de código. Em Zig, a mesma funcionalidade é implementada com reflexão em comptime usando funções como @typeInfo — e qualquer erro aponta diretamente para a linha do seu código.
Para mais comparações entre Zig e Rust, confira Zig vs Rust.
Comptime vs C++ Templates
Templates C++ são Turing-complete, mas formam uma sublinguagem funcional acidentalmente complexa. Em C++20, conceitos (concepts) melhoraram a situação, mas a complexidade fundamental permanece.
O comptime de Zig oferece o mesmo poder com uma fração da complexidade:
- C++ Templates: linguagem funcional implícita com regras de SFINAE
- Zig Comptime: código imperativo normal executado em compilação
A diferença na prática é brutal: o código comptime de Zig é legível para qualquer programador que conheça a linguagem, enquanto metaprogramação com templates C++ exige conhecimento especializado.
Reflexão em Tempo de Compilação
Zig oferece reflexão completa via builtins como @typeInfo, @typeName e @hasField. Isso permite introspecção de tipos sem nenhum custo em runtime:
fn imprimirCampos(comptime T: type) void {
const info = @typeInfo(T);
switch (info) {
.@"struct" => |s| {
inline for (s.fields) |campo| {
std.debug.print("Campo: {s}, Tipo: {s}\n", .{
campo.name,
@typeName(campo.type),
});
}
},
else => @compileError("Esperado uma struct"),
}
}
Essa função imprime todos os campos de qualquer struct em tempo de compilação. O inline for desenrola o loop, gerando código específico para cada campo — zero overhead em runtime.
Boas Práticas com Comptime
- Use comptime para eliminar branches em runtime — se uma decisão pode ser tomada na compilação, marque o parâmetro como
comptime - Prefira
@compileErrora erros em runtime — falhe cedo, falhe claro - Documente funções que retornam tipos — o leitor precisa saber que
MeuTipo(u32, 10)gera um tipo novo - Cuidado com tempos de compilação — loops comptime muito grandes podem aumentar o tempo de build
- Use
inline forcom moderação — desenrolar loops grandes pode inflar o binário
Conclusão
O comptime é o que torna Zig único no ecossistema de linguagens de sistemas. Enquanto C depende de um preprocessador primitivo, C++ de templates acidentalmente complexos e Rust de macros procedurais separadas, Zig oferece metaprogramação usando a mesma linguagem que você já conhece.
Se você está começando com Zig, o comptime pode parecer exótico. Mas à medida que você ganha experiência, percebe que ele é a ferramenta mais natural e poderosa para escrever código genérico, validar invariantes e otimizar performance — tudo em tempo de compilação.
Quer saber mais sobre por que aprender Zig? Confira nossos outros artigos e o glossário completo para se aprofundar na linguagem.