
Забудьте про скучные «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 Processors
Java предоставляет SPI для процессоров аннотаций через javax.annotation.processing.Processor (чаще всего вы наследуетесь от AbstractProcessor).
Процесс выглядит так:
- С помощью @SupportedAnnotationTypes(«org.example.Builder») или переопределения метода getSupportedAnnotationTypes() указываете, какие аннотации обрабатываете.
- В методе process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) получаете все элементы, отмеченные вашей аннотацией (например, @ Builder на классах).
- С помощью 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 💻

