
Забудьте про скучные «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 ?

Jijiki
я немного не в тему наверно но об посмотреть как собирает, и включениях
развернуть код с включениями можно при помощи clang-tools там надо посмотреть(она есть там инклюды показывает и тд), какая тула развернёт, далее при обращении к lsp решению (я не в вс код смотрел)(тут поможет немного магии, дело в том что lsp может показать как компилирует, парадокс в том что он для отрисовки), делая текстовый редактор смотрел как реализовать подсказки и как-бы столкнулся с ситуацией )) можно посмотреть как компилируется, ну и там надо в тулах посмотреть есть еще cov и прочее, правда для этого придётся собирать кланг(всё это конечно не до конца покажет что там внутри)
я теста ради всё прокинул тоже в темплейты, всё ускорилось, но у меня и зависимостей нету
--
ну а можно просто -v прописать тоже что-то покажет как компилирует, как это применить в визуалке не знаю возможно поможет cl --help
Jijiki
json пока не буду использовать, dsl пусть она сама генерит если ей надо с АСТ я пока акцентируюсь на оснастках и показать как собирается и тп, мне лсп не совсем и нужен при условии если он в терминале будет, если заработает как я предпологаю
просто в java комплит кода дорогой очень и клангд дорогой если понаблюдать
у меня будет решение наоборот наверно, я сужу по емаксу и еклипсу, и там и там висит клангд и много памяти потребляет