- PVSM.RU - https://www.pvsm.ru -
Предлагается разработать безопасную альтернативу встроенного макроса __COUNTER__. Первое вхождение макроса заменяется на 0, второе на 1, и так далее. Значение __COUNTER__ подставляется на этапе препроцессирования, следовательно его можно использовать в контексте constant expression.
К сожалению, макрос __COUNTER__ опасно использовать в заголовочных файлах — при другом порядке включения заголовочных файлов подставленные значения счетчика поменяются. Это может привести к ситуации, когда например в foo.cpp значение константы AWESOME равно 42, в то время как в bar.cpp AWESOME≡33. Это нарушение принципа one definition rule, что есть страшный криминал во вселенной C++.
Нужна возможность использовать локальные счетчики вместо единого глобального (как минимум, для каждого заголовочного файла свой). При этом возможность использовать значение счетчика в constant expression должна сохраниться.
По мотивам этого вопроса [1] на Stack Overflow.
STRUCT(Point3D)
FIELD(x, float)
FIELD(y, float)
FIELD(z, float)
END_STRUCT
Здесь мы не просто определяем структуру Point3D со списком полей x, y и z. Мы также автоматически получаем функции сериализации и десериализации. Невозможно добавить новое поле, и забыть для него поддержку сериализации. Писать приходится значительно меньше, чем например для boost [2].
К сожалению, список полей нам потребуется пройти как минимум два раза: чтобы сформировать определения полей и чтобы сгенерировать функцию сериализации. С помощью одного только препроцессора это сделать невозможно. Но как известно, любую проблему в 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четчик помогает связать разрозненные функции вместе.
По ссылке [3] рабочий пример кода.
Для начала, покажем реализацию однобитного счетчика.
// (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-битный счетчик строится на тех же принципах, что и однобитный. Вместо одного вызова 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].
Окончательный вариант кода доступен по ссылке [4].
Счетчик времени компиляции целиком основан на механизме перегруженных функций. Эту технику я подсмотрел [1] на Stack Overflow. Как правило, нетривиальные [5] вычисления времени компиляции в C++ реализуются на шаблонах, именно поэтому представленное решение особенно интересно, так как вместо шаблонов эксплуатирует иные механизмы.
Насколько такие решения практичны?
ИМХО если единственный C++ файл компилируется более 5 минут, причем справиться с ним может только самая последняя версия компилятора — это точно непрактично [6]. Многие «креативные» варианты использования языковых возможностей в C++ представляют исключительно академический интерес. Как правило, те же задачи можно лучше решить иными способами, например путем привлечения внешнего кодогенератора. Хотя, надо сказать, автор несколько предвзят в данном вопросе, категорически не признавая spirit [7], и испытывая некоторую слабость по отношению к bison [8].
Кажется, счетчик времени компиляции так же не особо практичен, как хорошо видно на следующем графике. По оси x отложена абсолютная величина приращения счетчика в тестовой программе (тестовая программа состоит из строк counter_inc(int)), по оси y — время компиляции в секундах. Для сравнения, там же отложено время компиляции nginx-1.5.2.
Автор: mejedi
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/37871
Ссылки в тексте:
[1] этого вопроса: http://stackoverflow.com/questions/6166337/does-c-support-compile-time-counters
[2] boost: http://www.boost.org/doc/libs/1_53_0/libs/serialization/doc/tutorial.html#serializablemembers
[3] ссылке: https://gist.github.com/mejedi/5875719
[4] ссылке: https://gist.github.com/mejedi/5908409
[5] нетривиальные: http://ubietylab.net/ubigraph/content/Papers/pdf/CppTuring.pdf
[6] непрактично: http://habrahabr.ru/post/182428/
[7] spirit: http://www.boost.org/doc/libs/1_54_0/libs/spirit/doc/html/index.html
[8] bison: http://www.gnu.org/software/bison/
[9] Источник: http://habrahabr.ru/post/184800/
Нажмите здесь для печати.