Предлагается разработать безопасную альтернативу встроенного макроса __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, то следующей нужно переключить стрелку M[(i+1)&~i, (i+1)&i].
Окончательный вариант кода доступен по ссылке.
Вместо заключения
Счетчик времени компиляции целиком основан на механизме перегруженных функций. Эту технику я подсмотрел на Stack Overflow. Как правило, нетривиальные вычисления времени компиляции в C++ реализуются на шаблонах, именно поэтому представленное решение особенно интересно, так как вместо шаблонов эксплуатирует иные механизмы.
Насколько такие решения практичны?
ИМХО если единственный C++ файл компилируется более 5 минут, причем справиться с ним может только самая последняя версия компилятора — это точно непрактично. Многие «креативные» варианты использования языковых возможностей в C++ представляют исключительно академический интерес. Как правило, те же задачи можно лучше решить иными способами, например путем привлечения внешнего кодогенератора. Хотя, надо сказать, автор несколько предвзят в данном вопросе, категорически не признавая spirit, и испытывая некоторую слабость по отношению к bison.
Кажется, счетчик времени компиляции так же не особо практичен, как хорошо видно на следующем графике. По оси x отложена абсолютная величина приращения счетчика в тестовой программе (тестовая программа состоит из строк counter_inc(int)
), по оси y — время компиляции в секундах. Для сравнения, там же отложено время компиляции nginx-1.5.2.
Автор: mejedi