Как известно, первым этапом компиляции C и C++ является препроцессор, который заменяет макросы и директивы препроцессора простым текстом.
Это позволяет делать нам странные вещи, например, такие:
// xmacro.h "look, I'm a string!" // xmacro.cpp std::string str = #include "xmacro.h" ;
После работы препроцессора это недоразумение превратится в корректный код:
std::string str = "look, I'm a string!" ;
Само собой, никуда более этот страшный header инклудить нельзя. И да, в связи с тем, что мы будем этот header добавлять несколько раз в один и тот же файл — никаких #pragma once или include guard-ов.
Собственно, давайте напишем более сложный пример, который будет делать разные вещи при помощи макросов и заодно защитимся от случайных #include:
// xmacro.h #ifndef XMACRO #error "Never include me directly" #endif XMACRO(first) XMACRO(second) #undef XMACRO // xmacro.cpp enum class xenum { #define XMACRO(x) x, #include "xmacro.h" }; std::ostream& operator<<(std::ostream& os, xenum enm) { switch (enm) { #define XMACRO(x) case xenum::x: os << "xenum::" #x; break; #include "xmacro.h" } return os; }
Это всё так же некрасиво, но некий шарм уже появляется: при добавлении нового элемента в enum class он автоматически добавится и в перегруженный оператор вывода.
Здесь же можно формализировать ареал применения данного метода: необходимость кодогенерации в разных местах на основе одного источника.
А теперь грустная история о X-Macro и Windows. Есть такая система как Windows Performance Counters, позволяющая отдавать некие счетчики в операционную систему, чтобы другие приложения могли их забирать. Например, Zabbix можно настроить на сбор и мониторинг любых Performance Counter-ов. Это достаточно удобно, и не нужно изобретать велосипед с отдачей\запросом данных.
Я искренне думал, что добавление нового счетчика выглядит а-ля HANDLE counter = AddCounter(«name»). Ах, как же я ошибался.
Для начала необходимо написать специальный XML-манифест (пример), или сгенерировать его программой ecmangen.exe из Windows SDK, но этот ecmangen почему-то удален из новых версий Windows 10 SDK. Далее надо сгенерировать сишный код и .rc файл при помощи утилиты ctrpp на основе нашего XML-манифеста. Само добавление новых счетчиков в систему делается только при помощи утилиты lodctr с нашим XML-манифестом в аргументе.
Perfcounters используют эти .rc файлы для локализации имён счетчиков, причем не очень понятно, зачем эти имена локализировать.
Суммируя вышесказанное: чтобы добавить 1 счетчик нужно:
- Изменить XML-манифест
- Сгенерировать новые .c и .rc файлы проекта на основе манифеста
- Написать новую функцию, которая будет инкрементить новый счетчик
- Написать новую функцию, которая будет забирать значение счетчика
Итого: 4-5 измененных файлов в diff-e ради одного счетчика и постоянное страдание от работы с XML-манифестом, являющимся источником информации в плюсовом коде. Это то, что нам предлагает Microsoft.
Собственно, придуманное решение выглядит страшно, однако добавление нового счетчика делается ровно 1 строчкой в одном файле. Далее всё генерируется автоматически при помощи макросов и, к сожалению, pre-build скрипта, так как XML-манифест все равно нужен, хоть он теперь и не является главным.
Наш perfcounters_ctr.h выглядит почти идентично примеру выше:
#ifndef NV_PERFCOUNTER #error "You cannot do this!" #endif ... NV_PERFCOUNTER(copied_bytes) NV_PERFCOUNTER(copied_files) ... #undef NV_PERFCOUNTER
Как я писал ранее, добавление счетчиков производится загрузкой XML-манифеста при помощи lodctr.exe. Из нашей программы мы можем их только инициализировать и изменять.
Интересные нам фрагменты инициализации в сгенерированном сишнике выглядят вот так:
#define COPIED_BYTES 0 // Счетчики всегда начинаются с 0 #define COPIED_FILES 1 // и далее инкрементируются на единичку const PERF_COUNTERSET_INFO counterset_info{ ... 2, // количество счетчиков в XML-манифесте захардкожено ... }; struct { PERF_COUNTERSET_INFO set; PERF_COUNTER_INFO counters[2]; // Захардкоженный размер статического массива } counterset { counterset_info, { // Сгенерированное описание каждого счетчика { COPIED_BYTES, ... }, { COPIED_FILES, ... } } }
Итого: нам нужно соответствие вида «имя счетчика — возрастающий индекс», а на этапе компиляции необходимо знать количество счетчиков и собрать массив инициализации из индексов счетчиков. Тут-то и приходит на помощь X-macro.
Сделать соответствие имени счетчика его возрастающему индексу достаточно просто.
Код ниже превратится в enum class, чьи внутренние индексы начинаются с 0, и инкрементируются на единичку. Добавив руками последний элемент, мы сразу узнаем сколько у нас суммарно счетчиков:
enum class counter_enum : int { #define NV_PERFCOUNTER(ctr) ctr, #include "perfcounters_ctr.h" total_counters };
И далее на основе нашего же enum-а нужно инициализировать счетчики:
static constexpr int counter_count = static_cast<int>(counter_enum::total_counters); const PERF_COUNTERSET_INFO counterset_info{ ... counter_count, ... }; struct { PERF_COUNTERSET_INFO set; PERF_COUNTER_INFO counters[counter_count]; } counterset { counterset_info, { // Сгенерированное описание каждого счетчика #define NV_PERFCOUNTER(ctr) \ { static_cast<int>(counter_enum::ctr), ... }, #include "perfcounters_ctr.h" } }
Результатом стало то, что инициализация нового счетчика теперь занимает 1 строку и не требует дополнительных изменений в других файлах (ранее каждая перегенерация меняла 3 куска кода только в инициализации).
И давайте добавим удобное API для инкремента счетчиков. Что-то в духе:
#define NV_PERFCOUNTER(ctr) \ inline void ctr##_tick(size_t diff = 1) { /* Увеличение счетчика counter_enum::ctr */ } #include "perfcounters_ctr.h" #define NV_PERFCOUNTER(ctr) \ inline size_t ctr##_get() { /* Возврат значения счетчика counter_enum::ctr */ } #include "perfcounter_ctr.h"
Препроцессор сгенерирует для нас красивые геттеры\сеттеры, которые мы сразу можем использовать в коде, например:
inline void copied_bytes_tick(size_t diff = 1); inline size_t copied_bytes_get();
Но у нас еще остались 2 грустные вещи: XML-манифест и .rc файл (увы, он необходим).
Мы сделали достаточно просто — pre-build скрипт, который читает изначальный файл с макросами, определяющими счетчики, парсит то, что находится между «NV_COUNTER(» и ")", и на основе этого генерирует оба файла, которые находятся в .gitignore, чтобы не засорять diff'ы.
Было: Специальный софт на основе XML-манифеста генерировал сишный код. Очень много изменений в проекте на каждое добавление\удаление счетчика.
Стало: Препроцессор и prebuild скрипт генерируют все счетчики, XML-манифест и .rc файл. Ровно одна строка в diff-e для добавления\удаления счетчика. Спасибо препроцессору, который помог решить эту задачу, показывая в данном конкретном кейсе больше пользы, чем вреда.

