__COUNTER__
. Первое вхождение макроса заменяется на 0
, второе на 1
, и так далее. Значение __COUNTER__
подставляется на этапе препроцессирования, следовательно его можно использовать в контексте constant expression.
К сожалению, макрос __COUNTER__
опасно использовать в заголовочных файлах — при другом порядке включения заголовочных файлов подставленные значения счетчика поменяются. Это может привести к ситуации, когда например в foo.cpp
значение константы AWESOME
равно 42, в то время как в bar.cpp
AWESOME≡33
. Это нарушение принципа one definition rule, что есть страшный криминал во вселенной C++.
Нужна возможность использовать локальные счетчики вместо единого глобального (как минимум, для каждого заголовочного файла свой). При этом возможность использовать значение счетчика в constant expression должна сохраниться.
По мотивам этого вопроса на Stack Overflow.
Мотивирующий пример
STRUCT(Point3D) FIELD(x, float) FIELD(y, float) FIELD(z, float) END_STRUCT
Здесь мы не просто определяем структуру Point3D
со списком полей x, y
и z
. Мы также автоматически получаем функции сериализации и десериализации. Невозможно добавить новое поле, и забыть для него поддержку сериализации. Писать приходится значительно меньше, чем например для boost.
К сожалению, список полей нам потребуется пройти как минимум два раза: чтобы сформировать определения полей и чтобы сгенерировать функцию сериализации. С помощью одного только препроцессора это сделать невозможно. Но как известно, любую проблему в C++ можно решить с помощью шаблонов (кроме проблемы переизбытка шаблонов).
Определим макрос FIELD
следующим образом (для наглядности используем __COUNTER__
):
#define FIELD(name, type) \ type name; // определение поля \ template<> \ void serialize<__COUNTER__/2>(Archive &ar) { \ ar.write(name); \ serialize<(__COUNTER__-1)/2+1>(ar); \ }
При разворачивании FIELD(x, float)
получится
float x; // определение поля x template<> void serialize<0>(Archive &ar) { ar.write(x); serialize<1>(ar); }
При разворачивании FIELD(y, float)
получается
float y; // определение поля y template<> void serialize<1>(Archive &ar) { ar.write(x); serialize<2>(ar); }
Каждое последующее вхождение макроса FIELD()
разворачивается в определение поля, плюс специализацию функции serialize<
i>()
где i=0,1,2,…N. Функция serialize<i>()
вызывает serialize<i+1>()
, и так далее. Cчетчик помогает связать разрозненные функции вместе.
По ссылке рабочий пример кода.
Однобитный счетчик времени компиляции
Для начала, покажем реализацию однобитного счетчика.
// (1) template<size_t n> struct cn { char data[n+1]; }; // (2) template<size_t n> cn<n> magic(cn<n>); // (3) текущее значение счетчика sizeof(magic(cn<0>())) - 1; // 0 // (4) «инкремент» cn<1> magic(cn<0>); // (5) текущее значение счетчика sizeof(magic(cn<0>())) - 1; // 1
- Определяем шаблонную структуру
cn<n>
. Отметим, чтоsizeof(cn<n>) ≡ n+1
. - Определяем шаблонную функцию
magic
. - Оператор
sizeof
, примененный к выражению, выдает размер типа, который имеет данное выражения. Так как выражение не вычисляется, определения тела функцииmagic
не требуется.
Единственная определенная на данный момент функцияmagic
— шаблон из п. 2. Поэтому тип возвращаемого значения и всего выражения —cn<0>
. - Определим перегруженную функцию
magic
. Отметим, что неоднозначности при вызовеmagic
не возникает, потому что перегруженные функции имеют приоритет перед шаблонными функциями. - Теперь при вызове
magic(cn<0>())
будет использован другой вариант функции; тип выражения внутриsizeof
— .cn<
1>()
Резюмируя — имеем выражение с вызовом функции. Добавляем определение перегруженной функции, в результате компилятор использует новую функцию. Таким образом, тип возвращаемого значения из функции и тип всего выражения изменился, хотя текстуально выражение осталось прежним.
Определим макросы для чтения и «инкрементации» однобитного счетчика.
#define counter_read(id) \ (sizeof(magic(cn<0>())) - 1) #define counter_inc(id) \ cn<1> magic(cn<0>)
magic
должна принимать дополнительный параметр id
. Перегруженные функции magic
будут относится к конкретному id, и не будут влиять на все остальные id. N-битный счетчик времени компиляции
N-битный счетчик строится на тех же принципах, что и однобитный. Вместо одного вызова magic
внутри sizeof
у нас будет цепочка вложенных вызовов a(b(c(d(e( … ))))).
Вот он, наш базовый строительный блок. Это функция от одного аргумента типа T0. В зависимости от доступных деклараций в области видимости, тип возвращаемого значения или T0 или T1. Это устройство напоминает стрелку на железной дороге. В начальном состоянии, «стрелка» направлена влево. «Стрелку» можно переключить единственный раз.
Используя несколько базовых блоков, мы можем собрать разветвленную сеть:
При поиске подходящего варианта функции, компилятор C++ учитывает только типы параметров а тип возврашаемого значения игнорирует. Если в выражении есть вложенные вызовы функций, компилятор «движется» изнутри наружу. Например в следующем выражении: M1(M2(M4( T0() ))), компилятор сначала разрешает («резолвит») вызов функции M4(T0). Затем, в зависимости от типа возвращаемого значения функции M4, он разрешает вызов M2(T0) или M2(T4), и так далее.
Продолжая железнодорожную аналогию, можно сказать, что компилятор движется по железнодорожной сети сверху вниз, «сворачивая» на стрелках вправо или влево. Выражение из N вложенных вызовов функций порождает сеть с 2N выходами. Переключая стрелки в правильном порядке, можно последовательно получить все 2N возможных типов Ti на выходе сети.
Можно показать, что если текущий тип на выходе сети Ti, то следующей нужно переключить стрелку
Окончательный вариант кода доступен по ссылке.
Вместо заключения
Счетчик времени компиляции целиком основан на механизме перегруженных функций. Эту технику я подсмотрел на Stack Overflow. Как правило, нетривиальные вычисления времени компиляции в C++ реализуются на шаблонах, именно поэтому представленное решение особенно интересно, так как вместо шаблонов эксплуатирует иные механизмы.
Насколько такие решения практичны?
ИМХО если единственный C++ файл компилируется более 5 минут, причем справиться с ним может только самая последняя версия компилятора — это точно непрактично. Многие «креативные» варианты использования языковых возможностей в C++ представляют исключительно академический интерес. Как правило, те же задачи можно лучше решить иными способами, например путем привлечения внешнего кодогенератора. Хотя, надо сказать, автор несколько предвзят в данном вопросе, категорически не признавая spirit, и испытывая некоторую слабость по отношению к bison.
Кажется, счетчик времени компиляции так же не особо практичен, как хорошо видно на следующем графике. По оси x отложена абсолютная величина приращения счетчика в тестовой программе (тестовая программа состоит из строк counter_inc(int)
), по оси y — время в секундах. Для сравнения, там же отложено время компиляции nginx-1.5.2.
ссылка на оригинал статьи http://habrahabr.ru/post/184800/
Добавить комментарий