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 нестандартен.

Я столкнулся с странной ситуацией, которую не совсем понимаю.

... и здесь я могу помочь. Во-первых, давайте рассмотрим правила работы препроцессора в целом.

Контур

Расширение функции макросов происходит в несколько этапов, которые мы можем назвать

  1. идентификация аргумента
  2. подстановка аргументов
  3. стрингификация и приклеивание
  4. повторное сканирование и дальнейшая замена

Во время идентификации аргумента вы просто сопоставляете формальные аргументы с вызванными аргументами. Для макросов с переменным аргументом стандарт требует, чтобы сам изменяющийся аргумент имел один или несколько вызываемых аргументов.

... как расширение 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);
Другие вопросы по тегам