Забудьте про скучные «Hello, World». Макросы и шаблоны давно стали полноценными инструментами архитектора кода: от хитрых C++-шаблонов до процедурных макросов Rust и Java-аннотаций, автоматически генерирующих целые фреймворки. 

В этой статье мы рассмотрим примеры, где metaprogramming избавляет от рутины и экономит часы работы над проектом. Детали как всегда под катом.

Торопитесь? Просто кликните по нужному разделу:
Template-less метапрограммирование: от классических TMP-хаков к «value-based» подходу
Rust-макросы: procedural-магия и её живые примеры
Java Annotation Processors в бою
Куда двигаться дальше

Template-less метапрограммирование: от классических TMP-хаков к «value-based» подходу


Перед тем как нырнуть в код, давайте признаем: классическое метапрограммирование в C++ часто превращается в болото рекурсивных шаблонных инстанциаций. IDE начинает тормозить, а вы всё сильнее скучаете по простому и понятному коду. 

При этом именно TMP дарит нам гибкость таких арсеналов, как std::variant и std::tuple, позволяет оптимально упаковывать структуры и скрывать массу других «фишек» STL. Но можно ли сохранить всю эту мощь и одновременно избавиться от шаблонного нагромождения? Достаточно сопоставить каждому типу уникальное constexpr-значение и дальше оперировать привычными массивами, циклами и std::ranges вместо std::tuple и std::conditional_t, такое показали и на CppCon 2024.

Давайте посмотрим четыре реальных примера, где «template-less» подход не просто упрощает код, но и ускоряет компиляцию.

Источник

Предлагаю перейти к «value-based» подходу, о котором говорилось в 2024–2025 годах на CppCon. Всё строится вокруг простой идеи: каждому типу T соответствует уникальное constexpr-значение, и дальше мы работаем с обычными массивами и диапазонами, а не с std::tuple и std::conditional_t.

Value-based TMP впервые появляется, когда мы собираем «мета-значения» в однородный контейнер:

// Value-based TMP — собираем мета-значения в std::array
std::array ts(<int>, <void>}; // ?
template<class T> struct type { 
   static void id(); // или static constexpr variable... 
};
template<class T>
inline constexpr auto meta = type<T>::id;
static_assert(meta<int> == meta<int>);
static_assert(meta<int> != meta<void>);
static_assert(typeid(meta<int>) == typeid(meta<void>));
std::array ts { meta<int>, meta<void> };


Теперь вместо кучи рекурсий по Ts… мы держим в руках простой std::array из адресов функций, каждый из которых однозначно указывает на свой тип.

Дальше, когда нужно отобрать подмножество типов по некоторому условию — например, собрать std::variant только из тех Ts, которые можно сконструировать из T — мы не пишем бесконечные enable_if и std::tuple_cat, а используем std::ranges:

// Value-based TMP — variant_for через std::ranges (C++20)
template<class T>
constexpr auto variant_for(const std::ranges::range auto& ts)
    -> std::ranges::range auto{
    auto&& r = ts
        | std::views::filter(is_constructible<T>)
        | std::views::transform(remove_cvref)
        | std::ranges::to<std::vector>()
;
    std::ranges::sort(r);
    r.erase(std::ranges::unique(r), r.end());
    return r;
}
template<class T>
constexpr auto is_constructible = [](auto t) {
    return invoke<std::is_constructible, T>(t);
};
// ...

Фильтруем, трансформируем, сортируем, уникализируем — и вуаля, набор типов готов. Ошибки компиляции укажут прямо на filter или transform, а не на сотни строк расползающихся инстанциаций.

Но TMP — это не только про типы, но и про оптимизацию структур. Вот классическая проблема паддингов на x86-64:

// [Examples] Performance / Memory
struct unpacked {
    char a; static_assert(sizeof(a) == 1u); // x86-64
    int  b; static_assert(sizeof(b) == 4u); // x86-64
    char c; static_assert(sizeof(c) == 1u); // x86-64
};
/*
 * https://eel.is/c++draft/basic.align
 */
static_assert(12u == sizeof(unpacked));
static_assert(8u == sizeof(pack_t<unpacked>)); // Powered by TMP
static_assert(
    requires (pack_t<unpacked> p) { // Powered by TMP
        p.a;
        p.b;
        p.c;
    }
);


С pack_t TMP на этапе компиляции перебирает поля, сортирует их по выравниванию и генерирует упакованную структуру размером 8 байт вместо 12. И снова без рекурсивных шаблонов, а чистый constexpr код.

Наконец, взгляд на то, что в TMP прячется прямо в STL:

// [Examples] Standard Template Library (STL)
template<class... Ts>
template<class T>
constexpr variant<Ts...>::variant(T&& t)
    : index{ find_index<T, Ts...> } // Powered by TMP
, //
{ }
template<size_t I, class... Ts>
constexpr auto get(tuple<Ts...>&&) noexcept  ->
    typename tuple_element<I, tuple<Ts...>>::type&&; // Powered by TMP
template<class TFirst, class... TRest>
array(TFirst, TRest...) -> array<
        typename Enforce_same<TFirst, TRest...>::type, // Powered by TMP
        1 + sizeof...(TRest)
       >;


Это не какой-то экзотический хак: в каждой строчке мы видим TMP-механизмы, которые C++ тащит из коробки. Но теперь представьте, что вместо этих хитросплетений можно писать чуть-чуть «meta + ranges + constexpr» и во многом победить шаблонные сложности.

Итог: «template-less» метапрограммирование переносит TMP-логику в мир значений и диапазонов. Оно ускоряет компиляцию, упрощает отладку и позволяет писать почти обычный C++ вместо вечного пляса с рекурсивными шаблонами.

В вашем проекте гибкие интерфейсы с большим количеством типов и вы страдаете от долгих сборок? Самое время попробовать «meta + constexpr» подход.


Источник

Rust-макросы: procedural-магия и примеры


Когда вы в очередной раз копируете блок кода с небольшими изменениями или вынуждены вручную поддерживать список CLI-команд, REST-эндпоинтов или версионированных полей — настал момент вспомнить о макросах Rust. Они позволяют писать не просто функции, а мини-программы, генерирующие код прямо на этапе компиляции. Впечатляющая мощь, но при этом вполне контролируемая, если знать несколько приёмов.

▍ Пример создания пользовательского макроса


Декларативные макросы работают на уровне токенов: вы описываете шаблон, а компилятор подставляет нужные фрагменты. Давайте рассмотрим простой пример написания пользовательского макроса. Макросы Rust определяются с помощью синтаксиса macro_rules!.. Вот макрос, который создает вектор и помещает в него некоторые элементы:

macro_rules! create_vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}
fn main() {
    let v = create_vec![1, 2, 3, 4];
    println!(»{:?}», v);
}


Сreate_vec! макрос берет произвольное количество выражений ( $x:expr) и генерирует код. Здесь $( $x:expr ),* — это оператор повторения: он принимает любое число выражений и для каждого разворачивает temp_vec.push($x). Никаких ручных циклов, только лаконичный DSL прямо в исходнике.

▍ Процедурные макросы для runtime-безопасности


Если вам нужно не просто текстовый макрос, а вставить логику на уровне AST, приходят на помощь процедурные макросы (proc_macro). Рассмотрим ключевые кейсы из присланного материала:

Атрибут #[trace] — автоматически логирует вход и выход функций:

use trace::trace;
#[trace]
fn compute(x: i32, y: i32) -> i32 {
    x * y + 42
}
// При вызове compute(2, 3) в лог попадёт:
// «Entering compute with args: (2, 3)»
// «Exiting compute => 48»


Автосериализация структур с версионированием — комбинируем #[derive(Serialize)] и собственный #[version = 2], чтобы разные поля учитывались по-разному:

#[derive(Serialize, Versioned)]
#[version = 2]
struct User {
    id: u64,
    #[version = 1]
    name: String,
    #[version = 2]
    email: Option<String>,
}


Регистрация REST-эндпоинтов через атрибуты:

#[rest(»/users», get)]
fn list_users() -> Json<Vec<User>> { /* … */ }
#[rest(»/users», post)]
fn create_user(new: Json<NewUser>) -> Json<User> { /* … */ }

Процедурные макросы парсят ваши структуры или функции, встраивают код регистрации маршрутов, логирования или сериализации — и при этом вы пишете лишь привычные Rust-функции и структуры.

▍ Подводные камни и лайфхаки


  • Кешируйте AST-парсинг. Каждый вызов proc_macro парсит токены заново через proc_macro2, что может ощутимо замедлить компиляцию. Решение — хранить распарсенные данные в lazy_static или once_cell.

  • Проверяйте вывод макросов. В одном проекте процедурный макрос незаметно встраивал неразрывные пробелы в генерируемый код. Компиляция проходила, но на проде JSON-парсер упорно падал. После cargo expand и просмотра через od -c все артефакты стали видны.

Макросы Rust дают фантастическую гибкость: от написания мини-DSL на macro_rules! до мощных AST-трансформаций в proc_macro.  Однако помните золотое правило: сила требует мудрости. Уважайте инструмент, не злоупотребляйте им — тогда ваш код останется чистым, быстрым и очень выразительным. Ведь как говаривал дядя Бен: «С великой силой приходит и великая ответственность».

Java Annotation Processors в бою


В мире Java регулярно приходится писать однотипный «бытовой» код — геттеры/сеттеры, DTO-мэппинги, REST-эндпоинты и т. п. Вручную поддерживать его неудобно и чревато ошибками, поэтому уже давно придуманы инструменты генерации. Различают однократную генерацию (IDE-шаблоны, геттеры/сеттеры) и непрерывную: изменение спецификации OpenAPI, аннотаций или интерфейса автоматически порождает новый код при каждой компиляции.

MapStruct

Один из самых популярных примеров — MapStruct. Вы пишете интерфейс:

@Mapper
public interface CompanyMapper {
    CompanyMapper INSTANCE = Mappers.getMapper(CompanyMapper.class);
    @Mapping(target = «companyName», source = «name»)
    @Mapping(target = «companyAge»,  source = «age»)
    CompanyDto map(Company company);
}
Gradle-конфигурация сообщает компилятору, что нужен процессор аннотаций:
dependencies {
    annotationProcessor «org.mapstruct:mapstruct-processor:${mapstructVersion}»
}


MapStruct во время компиляции генерирует класс CompanyMapperImpl, где метод map(...) развёртывает все передачи полей и проверки на null, избавляя вас от ручного написания одинаковых строк.

Цикл работы Annotation Processor'а: аннотация → генерация кода → повтор компиляции. Источник

Собственные Annotation Processors

Java предоставляет SPI для процессоров аннотаций через javax.annotation.processing.Processor (чаще всего вы наследуетесь от AbstractProcessor). 

Процесс выглядит так:

  1. С помощью @SupportedAnnotationTypes(«org.example.Builder») или переопределения метода getSupportedAnnotationTypes() указываете, какие аннотации обрабатываете.

  2. В методе process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) получаете все элементы, отмеченные вашей аннотацией (например, @ Builder на классах).

  3. С помощью processingEnv.getFiler().createSourceFile(...) создаёте новый .java-файл и записываете в него сгенерированный код через обычный Writer.

@SupportedAnnotationTypes(«org.example.Builder»)
public class BuilderAnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations,
                           RoundEnvironment roundEnv) {
        for (Element e : roundEnv.getElementsAnnotatedWith(Builder.class)) {
            String className = e.getSimpleName().toString();
            generateBuilderFor(className);
        }
        return true;
    }
    private void generateBuilderFor(String className) {
        try {
            JavaFileObject src = processingEnv.getFiler()
                .createSourceFile(className + «Builder»);
            try (Writer w = src.openWriter()) {
                w.write( /* текст класса билдера */ );
            }
        } catch (IOException ignored) { }
    }
}


Чтобы процессор автоматически регистрировался, добавьте в ваш JAR файл META-INF/services/javax.annotation.processing.Processor со строкой:

org.example.BuilderAnnotationProcessor

Или используйте Google AutoService, который сгенерирует этот файл за вас.

▍ Преимущества и сценарии использования


  • Быстрая синхронизация спецификации и кода. Правки в интерфейсе или аннотациях сразу отражаются в сгенерированных классах.

  • Чистый репозиторий. Тривиальный код (геттеры, сеттеры, мэппинги) не хранится в VCS, а живёт в артефактe, снижая шум при ревью.

  • Гибкость. MapStruct, Lombok, AutoValue, собственные процессоры — каждый выбирает свою стратегию, но общий принцип один: разметил аннотацию — получил готовый код.

Генерация через аннотации сегодня — стандартный путь борьбы с шаблонным кодом. Хотите избавить команду от сотен строк рутинных методов? Добавьте пару аннотаций, и Java Compiler сам всё сделает.

Куда двигаться дальше


Источник

В 2024–2025 годах метапрограммирование стало не просто способом сократить код, а полноценной парадигмой. Rust-макросы, C++ TMP, Java-аннотации, все они обещают одно: «Сделайте это один раз, и рутина исчезнет». На деле получаете 3–5 раз меньше boilerplate. Например, современные Rust-макросы или C++-TMP позволяют описывать бизнес-логику декларативно, а всю рутину — регистрацию эндпоинтов, сериализацию, проверку контрактов — брать на себя на этапе компиляции. Этот эффект подтверждается и в корпоративных отчётах, и в докладах на TeamleadConf 2024, где подчёркивается, что снижение ручного кода ускоряет внедрение новых фич и снижает количество ошибок, связанных с копипастом.

Второе преимущество — статическая проверка. Это как страховка, которая сработает на этапе сборки, а не в проде. В C++ с concepts или Rust через proc_macro вы заранее блокируете ошибки: если типы несовместимы, компилятор скажет «нет» сразу. Это даёт уверенность: ваш API не сломается из-за опечатки в методе, который «вроде бы должен быть». А еще сколько часов тестирования экономит — просто песня.

Однако у метапрограммирования есть и обратная сторона. Вы написали макрос, который генерирует код за вас. Отлично, пока он работает. А если сломается? Тогда начинается самое интересное: никто не помнит, как он устроен, а документация? Она вообще есть? Это затрудняет отладку и ревью, приводит к ситуации, когда один человек становится единственным экспертом по макросу, что создаёт кадровые риски. 

Ещё одна проблема — «невидимые зависимости»: генераторы кода могут скрывать логику от IDE, ломая автодополнение и рефакторинг. Новичкам в команде приходится разбираться, как всё это собирается, вместо того чтобы сразу приступить к фичам. В Java и других аннотационных языках чрезмерное наложение декораторов приводит к «декораторной путанице»: изменение одного класса может вызвать цепную реакцию изменений в сгенерированных классах, что сложно отследить и протестировать.

Метапрограммирование имеет смысл, когда вы повторяете одни и те же шаблоны кода и хотите централизовать их в едином «source of truth», получая при этом гарантии ещё на этапе компиляции. Но если проект совсем небольшой, а IDE не видит ваш сгенерированный код, вместо выгоды вы можете получить путаницу и потерю читаемости.

И да, в 2025-м AI-ассистенты уже неплохо справляются с написанием небольших функций по комментариям, а стартапы из области BCI обещают скоро давать возможность рисовать код в воздухе пальцем. Звучит как фантастика? В мире IT стоит ждать чего угодно.

А вы как справляетесь с рутиной в коде? Используете макросы, аннотации или полагаетесь на умных помощников? Расскажите в комментариях о своих находках и подводных камнях!

© 2025 ООО «МТ ФИНАНС»

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

Комментарии (3)


  1. Jijiki
    14.06.2025 19:52

    Ещё одна проблема — «невидимые зависимости»: генераторы кода могут скрывать логику от IDE, ломая автодополнение и рефакторинг.

    я немного не в тему наверно но об посмотреть как собирает, и включениях

    развернуть код с включениями можно при помощи clang-tools там надо посмотреть(она есть там инклюды показывает и тд), какая тула развернёт, далее при обращении к lsp решению (я не в вс код смотрел)(тут поможет немного магии, дело в том что lsp может показать как компилирует, парадокс в том что он для отрисовки), делая текстовый редактор смотрел как реализовать подсказки и как-бы столкнулся с ситуацией )) можно посмотреть как компилируется, ну и там надо в тулах посмотреть есть еще cov и прочее, правда для этого придётся собирать кланг(всё это конечно не до конца покажет что там внутри)

    я теста ради всё прокинул тоже в темплейты, всё ускорилось, но у меня и зависимостей нету

    --
    ну а можно просто -v прописать тоже что-то покажет как компилирует, как это применить в визуалке не знаю возможно поможет cl --help


    1. Jijiki
      14.06.2025 19:52

      json пока не буду использовать, dsl пусть она сама генерит если ей надо с АСТ я пока акцентируюсь на оснастках и показать как собирается и тп, мне лсп не совсем и нужен при условии если он в терминале будет, если заработает как я предпологаю

      просто в java комплит кода дорогой очень и клангд дорогой если понаблюдать

      у меня будет решение наоборот наверно, я сужу по емаксу и еклипсу, и там и там висит клангд и много памяти потребляет


  1. anonymous
    14.06.2025 19:52