GCC не удалось развернуть некоторые макросы
Я разрабатываю программу с использованием сторонней библиотеки пользовательского интерфейса с функциями в виде Vbox(void *first, ...)
, Они служат функциями макета и принимают произвольное количество параметров. Конец списка определяется первым обнаруженным значением NULL. Это означает, что мне нужно помнить, чтобы мой список заканчивался NULL, что я часто не могу сделать.
Поэтому я создал несколько вспомогательных макросов, которые должны расширяться, чтобы добавить в мой список значение NULL.
Они имеют форму:
#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)
##
перед __VA_ARGS__
служить, чтобы избавиться от предыдущей запятой в случае __VA_ARGS
пустой.
мне нужно first
в случае, если поле действительно инициализируется пустым (Vbox(NULL)
): в этих случаях пользователь должен явно добавить NULL, потому что я не могу избавиться от ,
после __VA_ARGS__
(так как ##
взлом работает только если запятая перед ##
, а не после), поэтому пользователь должен задать явное значение NULL, что приведет к следующему расширению: Vbox(NULL, NULL)
, что немного избыточно, но хорошо.
В целом, это хорошо работает, но я столкнулся со странной ситуацией, которую я не совсем понимаю.
Возьмите следующий файл, например:
// expand.c
void* Vbox(void* first, ...);
void* Hbox(void* first, ...);
#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)
#define UtlHbox(first, ...) Hbox(first, ##__VA_ARGS__, NULL)
static void* Test()
{
return UtlHbox(
Foo,
UtlVbox(
UtlHbox(Bar)));
}
Если я бегу gcc -E expand.c
Я получаю следующий вывод:
# 1 "expand.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "expand.c"
void* Vbox(void* first, ...);
void* Hbox(void* first, ...);
static void* Test()
{
return Hbox(Foo, Vbox(UtlHbox(Bar), NULL), NULL);
}
Все расширяется точно так, как ожидалось, за исключением самого внутреннего UtlHbox, который по некоторым причинам не был расширен и поэтому выдает ошибку при компиляции. (Кроме того, значения NULL не были расширены в этом примере, так как нет соответствующих #include). В VC12 (Visual Studio 2013) все компилируется просто отлично.
Что тут происходит? Является ли это конфликтом между различными значениями ##
операция? Есть ли способ решить это?
Я использую GCC 4.6.3, но я попытался скомпилировать это на GodBolt с GCC 7.1 и получить те же результаты.
После некоторых исследований я начинаю думать, что столкнулся с известной проблемой в GCC.
Кажется, что GCC спотыкается о себе. Если я создам третий макрос
#define UtlZbox(first, ...) Zbox(first , ##__VA_ARGS__, NULL)
и замените внутренний UtlHbox в приведенном выше примере на этот новый макрос, вывод правильный:
static void* Test()
{
return Hbox(Foo, Vbox(Zbox(Bar, NULL), NULL), NULL);
}
Похоже, что GCC спотыкается о себя, когда вариационный макрос повторяется в другом экземпляре самого себя.
Я провел несколько других тестов (модифицировал макросы для облегчения визуализации):
#define UtlVbox(first, ...) V(first,##__VA_ARGS__)
#define UtlHbox(first, ...) H(first,##__VA_ARGS__)
int main()
{
// HHH
UtlHbox(UtlHbox(UtlHbox(1)));
UtlHbox(UtlHbox(UtlHbox(2, 1)));
UtlHbox(UtlHbox(2, UtlHbox(1)));
UtlHbox(2, UtlHbox(UtlHbox(1)));
UtlHbox(3, UtlHbox(2, UtlHbox(1)));
// HHV
UtlHbox(UtlHbox(UtlVbox(1)));
UtlHbox(UtlHbox(UtlVbox(2, 1)));
UtlHbox(UtlHbox(2, UtlVbox(1)));
UtlHbox(2, UtlHbox(UtlVbox(1)));
UtlHbox(3, UtlHbox(2, UtlVbox(1)));
// HVH
UtlHbox(UtlVbox(UtlHbox(1)));
UtlHbox(UtlVbox(UtlHbox(2, 1)));
UtlHbox(UtlVbox(2, UtlHbox(1)));
UtlHbox(2, UtlVbox(UtlHbox(1)));
UtlHbox(3, UtlVbox(2, UtlHbox(1)));
// VHH
UtlVbox(UtlHbox(UtlHbox(1)));
UtlVbox(UtlHbox(UtlHbox(2, 1)));
UtlVbox(UtlHbox(2, UtlHbox(1)));
UtlVbox(2, UtlHbox(UtlHbox(1)));
UtlVbox(3, UtlHbox(2, UtlHbox(1)));
return 0;
}
Вот вывод Godbolt, скомпилированный с GCC 7.1 (выполнение этого на моей машине с 4.6.3 дает идентичный вывод):
Успешные преобразования отмечены зелеными стрелками, неудачи - красными. Кажется, что проблема заключается в том, что макрос X с переменными аргументами размещается где-либо в аргументах переменных другого экземпляра X (даже если он является аргументом (переменным или нет) какого-либо другого макроса Y).
Последний блок тестов (помечен как // Failures...
) является повторением всех предыдущих случаев, которые потерпели неудачу, заменяя только тот макрос, который не удалось развернуть, на UtlZbox. Выполнение этого вызвало правильное расширение в каждом отдельном случае, кроме случая, когда UtlZbox помещен в переменный аргумент другого UtlZbox.
1 ответ
Это не ошибка; это "синяя краска".
В VC12 (Visual Studio 2013) все компилируется нормально.
Напомню... Препроцессор Visual Studio нестандартен.
Я столкнулся с странной ситуацией, которую не совсем понимаю.
... и здесь я могу помочь. Во-первых, давайте рассмотрим правила работы препроцессора в целом.
Контур
Расширение функции макросов происходит в несколько этапов, которые мы можем назвать
- идентификация аргумента
- подстановка аргументов
- стрингификация и приклеивание
- повторное сканирование и дальнейшая замена
Во время идентификации аргумента вы просто сопоставляете формальные аргументы с вызванными аргументами. Для макросов с переменным аргументом стандарт требует, чтобы сам изменяющийся аргумент имел один или несколько вызываемых аргументов.
... как расширение GNU (которое вы используете) мы можем сопоставить изменяющуюся часть без аргумента. Я назову это пустым. Обратите внимание, что это отличается от пустых (и токенов-заполнителей); в частности, если мы#define FOO(x,...)
, то вызов FOO(z)
наборы __VA_ARGS__
в нуль; напротив,FOO(z,)
установит его пустым.
Во время замены аргумента вы применяете список замены; в списке замены вы можете заменить формальные аргументы вызванными аргументами. Перед этим любой вызываемый аргумент, который не преобразован в строку и также не участвует в операторе вставки (ни левая, ни правая часть вставки), полностью раскрывается.
Затем применяются стрингификация и вставка в любом порядке.
После выполнения вышеуказанных шагов будет еще одно заключительное сканирование на этапе повторного сканирования и дальнейшей замены. По особому правилу, во время сканирования определенного макроса вам больше не разрешается раскрывать тот же самый макрос. Стандартный жаргон для этого - "синяя краска"; макрос отмечен (или "окрашен синим") для этого расширения. По завершении всего сканирования макрос становится "неокрашенным".
Объяснение
Возьмем ваш первый пример, но я собираюсь его немного изменить:
#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)
#define UtlHbox(first, ...) Hbox(first, ##__VA_ARGS__, NULL)
#define foomacro Foo
UtlHbox(foomacro,UtlVbox(UtlHbox(Bar)))
Здесь я просто убираю букву "С", чтобы сосредоточиться только на препроцессоре. Я также изменил вызов для вызова макросаfoomacro
чтобы выделить что-то. Теперь вот как расширяется вызов UtlHbox.
Начнем с идентификации аргумента. UtlHbox
имеет формальные аргументы first
а также ...
; у вызова есть аргументыfoomacro
а также UtlVbox(UtlHbox(Bar))
. Такfirst
является foomacro
а также __VA_ARGS__
является UtlVbox(UtlHbox(Bar))
.
Затем мы выполняем замену аргументов, используя список замены, который:
Hbox(first, ##
__VA_ARGS__
, NULL)
... поэтому мы заменяем first
затем с foomacro
после foomacro
был расширен; а также__VA_ARGS__
с UtlVbox(UtlHbox(Bar))
буквально. Последний случай отличается, поскольку в этом списке замены__VA_ARGS__
является участником (а именно правой части) оператора вставки; таким образом, он не расширяется. Получаем вот что:
Hbox(Foo, ## UtlVbox(UtlHbox(Bar)))
Далее выполняем стрингификацию и вставку, получая следующее:
Hbox(Foo, UtlVbox(UtlHbox(Bar)))
Далее применяем повторное сканирование и дальнейшую замену дляUtlHbox
. Итак, рисуемUtlHbox
синий, затем мы оцениваем эту строку. Вы, наверное, уже видите, что здесь у вас проблемы, но для завершения я продолжу.
При повторном сканировании и дальнейшей замене находим UtlVbox
, который является другим макросом. Это дает второй уровень оценки макроса.UtlVbox
.
Во втором уровне идентификации аргумента, first
является UtlHbox(Bar)
; а также__VA_ARGS__
равно нулю.
На втором уровне подстановки аргументов мы смотрим на UtlVbox
список замены, который:
Vbox(first, ##__VA_ARGS__, NULL)
поскольку first
не преобразован в строку и не вставлен, мы оцениваем вызванный аргумент, UtlHbox(Bar)
, прежде чем заменить его. Но поскольку UtlHbox окрашен в синий цвет, мы не распознаем его как макрос. __VA_ARGS__
тем временем имеет значение null. Получаем просто:
Vbox(UtlHbox(Bar), ## null, NULL)
На втором уровне при вставке мы вставляем маркер размещения справа от запятой с нулем; это запускает расширение GNU для правила исключения запятой, поэтому полученная паста удаляет запятую, и мы получаем:
Vbox(UtlHbox(Bar), NULL)
На втором уровне повторное сканирование и замена красим UtlVbox
синий, затем повторно отсканируйте этот фрагмент. посколькуUtlHbox
это все еще окрашены в синий цвет, он до сих пор не признана в качестве макроса. Поскольку ничто иное не является макросом, сканирование завершается.
Итак, отступая на уровень, мы получаем следующее:
Hbox(Foo, Vbox(UtlHbox(Bar), NULL))
... перед тем, как продолжить, выполняя повторное сканирование и замену каждого, мы удаляем UtlVbox
а также UtlHbox
.
Решение
Есть ли способ решить эту проблему?
Обратите внимание, что есть два уровня расширения; один происходит во время замены аргумента, а другой - во время повторного сканирования и замены. Первое происходит до нанесения синей краски и может повторяться бесконечно:
#define BRACIFY(NAME_) { NAME_ }
BRACIFY(BRACIFY(BRACIFY(BRACIFY(BRACIFY(Z)))) BRACIFY(X))
... с радостью расширится до:
{ { { { { Z } } } } { X } }
Похоже, это то, что вы хотите сделать. Но оценка "подстановки аргументов" происходит только в том случае, если ваши аргументы не вставляются в строку или не вставляются. Итак, что вас действительно убивает, так это возможность исключения запятых в GNU; ваше использование этого включает применение оператора вставки к__VA_ARGS__
; это дисквалифицирует ваши различные аргументы в пользу расширения во время подстановки аргументов. Вместо этого они могут расширяться только во время повторного сканирования и замены, и на этом этапе ваш макрос окрашен в синий цвет.
Таким образом, решение состоит в том, чтобы просто избежать исключения запятых. В вашем случае это на самом деле довольно просто. Рассмотрим подробнее:
#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)
#define UtlHbox(first, ...) Hbox(first, ##__VA_ARGS__, NULL)
Так ты хочешь UtlVbox(a)
становиться Vbox(a, NULL)
, а также UtlVbox(a, b)
становиться Vbox(a, b, NULL)
. Как насчет того, чтобы просто сделать это?
#define UtlVbox(...) Vbox(__VA_ARGS__, NULL)
#define UtlHbox(...) Hbox(__VA_ARGS__, NULL)
Теперь это:
UtlHbox(UtlHbox(UtlHbox(1)));
UtlHbox(UtlHbox(UtlHbox(2, 1)));
UtlHbox(UtlHbox(2, UtlHbox(1)));
UtlHbox(2, UtlHbox(UtlHbox(1)));
UtlHbox(3, UtlHbox(2, UtlHbox(1)));
UtlHbox(UtlHbox(UtlVbox(1)));
UtlHbox(UtlHbox(UtlVbox(2, 1)));
UtlHbox(UtlHbox(2, UtlVbox(1)));
UtlHbox(2, UtlHbox(UtlVbox(1)));
UtlHbox(3, UtlHbox(2, UtlVbox(1)));
UtlHbox(UtlVbox(UtlHbox(1)));
UtlHbox(UtlVbox(UtlHbox(2, 1)));
UtlHbox(UtlVbox(2, UtlHbox(1)));
UtlHbox(2, UtlVbox(UtlHbox(1)));
UtlHbox(3, UtlVbox(2, UtlHbox(1)));
UtlVbox(UtlHbox(UtlHbox(1)));
UtlVbox(UtlHbox(UtlHbox(2, 1)));
UtlVbox(UtlHbox(2, UtlHbox(1)));
UtlVbox(2, UtlHbox(UtlHbox(1)));
UtlVbox(3, UtlHbox(2, UtlHbox(1)));
... расширяется до:
Hbox(Hbox(Hbox(1, NULL), NULL), NULL);
Hbox(Hbox(Hbox(2, 1, NULL), NULL), NULL);
Hbox(Hbox(2, Hbox(1, NULL), NULL), NULL);
Hbox(2, Hbox(Hbox(1, NULL), NULL), NULL);
Hbox(3, Hbox(2, Hbox(1, NULL), NULL), NULL);
Hbox(Hbox(Vbox(1, NULL), NULL), NULL);
Hbox(Hbox(Vbox(2, 1, NULL), NULL), NULL);
Hbox(Hbox(2, Vbox(1, NULL), NULL), NULL);
Hbox(2, Hbox(Vbox(1, NULL), NULL), NULL);
Hbox(3, Hbox(2, Vbox(1, NULL), NULL), NULL);
Hbox(Vbox(Hbox(1, NULL), NULL), NULL);
Hbox(Vbox(Hbox(2, 1, NULL), NULL), NULL);
Hbox(Vbox(2, Hbox(1, NULL), NULL), NULL);
Hbox(2, Vbox(Hbox(1, NULL), NULL), NULL);
Hbox(3, Vbox(2, Hbox(1, NULL), NULL), NULL);
Vbox(Hbox(Hbox(1, NULL), NULL), NULL);
Vbox(Hbox(Hbox(2, 1, NULL), NULL), NULL);
Vbox(Hbox(2, Hbox(1, NULL), NULL), NULL);
Vbox(2, Hbox(Hbox(1, NULL), NULL), NULL);
Vbox(3, Hbox(2, Hbox(1, NULL), NULL), NULL);