Метапрограммирование без макросов

Используем comptime для интроспекции типов и генерации кода без единого макроса.

Интроспекция типов — способность кода во время компиляции исследовать структуру типа (его поля, варианты, размер) и генерировать код в зависимости от этого; в Zig её даёт встроенная функция @typeInfo.

comptime — это не только дженерики. Поскольку при компиляции доступны типы как значения, можно исследовать их структуру и порождать код на лету. Это заменяет и макросы C, и кодогенераторы, и рефлексию — всё на одном языке.

Интроспекция через @typeInfo

const std = @import("std");

const Point = struct { x: i32, y: i32 };

pub fn main() void {
    const info = @typeInfo(Point);
    // перебираем поля структуры на этапе компиляции
    inline for (info.@"struct".fields) |field| {
        std.debug.print("поле: {s}\n", .{field.name});
    }
}

Вывод:

поле: x
поле: y

Функция @typeInfo возвращает структуру с описанием типа: для struct — список полей с именами и типами. Цикл inline for разворачивается при компиляции и проходит по полям. Так можно автоматически сгенерировать сериализацию, валидацию или вывод — без ручного перечисления полей.

inline for — развёртка при компиляции

Обычный for работает в рантайме. inline for же разворачивается компилятором: тело копируется для каждого элемента, и это позволяет внутри использовать значения, известные только при компиляции (например, имя поля как тип). Это мост между миром типов и обычным кодом.

Условная компиляция без #ifdef

const builtin = @import("builtin");

fn logIfDebug(msg: []const u8) void {
    // ветка выбирается на этапе компиляции по режиму сборки
    if (builtin.mode == .Debug) {
        std.debug.print("[debug] {s}\n", .{msg});
    }
    // в release ветка просто исчезает из бинарника
}

В C условную компиляцию делают через #ifdef. В Zig — обычным if по comptime-значению. Если условие известно при компиляции (как режим сборки), компилятор полностью убирает мёртвую ветку. Никакого препроцессора, тот же синтаксис, что и в рантайме.

Как работает под капотом

@typeInfo и подобные встроенные функции (@hasField, @field, @TypeOf) дают компилятору доступ к его внутреннему представлению типов в виде обычных Zig-значений. Поскольку всё это comptime-данные, генерация кода — это просто исполнение функций при компиляции с подстановкой результата. Отсюда и сила, и безопасность: метапрограммы пишутся и проверяются как обычный типизированный код, а не как текстовые макросы.

Частые ошибки

Первая — использовать обычный for там, где нужен inline for для перебора comptime-сущностей. Вторая — ждать рефлексии в рантайме: вся интроспекция Zig — на этапе компиляции, в бинарнике её следов нет. Третья — переусложнять comptime-логику: мощь соблазняет, но читаемость страдает, если перегнуть.

Итог

  • @typeInfo даёт структуру типа (поля, варианты) как comptime-значение.
  • inline for разворачивается при компиляции и связывает мир типов с обычным кодом.
  • Условную компиляцию делают обычным if по comptime-значению, без #ifdef.
  • Метапрограммы Zig — типизированный код при компиляции, а не текстовые макросы.
Проверьте себя
1. Что возвращает встроенная функция @typeInfo?
AИмя типа строкой
BСтруктуру с описанием типа (например, список полей) как comptime-значение
CРазмер типа в байтах
DУказатель на тип
2. Как в Zig делают условную компиляцию вместо #ifdef из C?
AЧерез макросы
BОбычным if по comptime-значению — мёртвая ветка убирается компилятором
CЧерез отдельные файлы
DЧерез рантайм-проверку флага