Переменные макросы с нулевыми аргументами не компилируются даже с ##__VA_ARGS__

#c #c-preprocessor #variadic-macros

#c #c-препроцессор #переменные макросы

Вопрос:

Если я попытаюсь скомпилировать следующий код:

 template <typename... TArgs>
void Dummy(const TArgs amp;...args)
{
}

#define DUMMY(...) Dummy("Hello", ##__VA_ARGS__)

int main()
{
    DUMMY();
}
  

Я получаю следующую ошибку компиляции:

 g   -std=c  17 -O3 -Wall main.cpp amp;amp; ./a.out
main.cpp: In function 'int main()':
main.cpp:6:48: error: expected primary-expression before ')' token
    6 | #define DUMMY(...) Dummy("Hello", ##__VA_ARGS__)
      |                                                ^
main.cpp:10:5: note: in expansion of macro 'DUMMY'
   10 |     DUMMY();
      |     ^~~~~
  

https://coliru.stacked-crooked.com/a/c9217ba86e7d24bd

Код компилируется нормально, когда я добавляю хотя бы один параметр:

 template <typename... TArgs>
void Dummy(const TArgs amp;...args)
{
}

#define DUMMY(dummy, ...) Dummy(dummy, ##__VA_ARGS__)

int main()
{
    DUMMY(); // This is strange. Why does this compile?
    DUMMY(1);
    DUMMY(1, 2);
    DUMMY(1, 2, 3);
}
  

https://coliru.stacked-crooked.com/a/e30e14810d70f482

Но я не уверен, что это правильно, потому что DUMMY принимает хотя бы один параметр, но я передаю ноль.

Комментарии:

1. Я рекомендую вам остановиться после предварительной обработки, чтобы посмотреть, до чего на самом деле расширяются макросы.

2. @Someprogrammerdude, Dummy("Hello",); 🙁 колиру .

Ответ №1:

Важным фактом о макросах C / C является то, что их невозможно вызвать без параметров, поскольку параметру макроса разрешается быть пустой последовательностью токенов.

Следовательно, DUMMY() вызывает макрос DUMMY с одним пустым параметром, а не с нулевыми параметрами. Это объясняет, почему работает второй пример, а также объясняет, почему первый пример выдает синтаксическую ошибку.

Расширение GCC удаляет запятую из, , ##__VA_ARGS__ когда __VA_ARGS__ в нем нет элементов. Но один пустой аргумент — это не то же самое, что отсутствие аргументов. Когда вы определяете DUMMY как #define DUMMY(...) , вы гарантируете, что у __VA_ARGS__ есть хотя бы один аргумент, поэтому , он не будет удален.

*** Примечание: GCC делает исключение из этого правила, если вы не указываете какой-либо стандарт ISO с --std опцией. В этом случае, если ... это единственный параметр макроса и вызов имеет пустой аргумент, то ,##__VA_ARGS__ запятая пропадает. Это указано в руководстве CPP в разделе Variadic Marcos:

Приведенное выше объяснение неоднозначно относится к случаю, когда единственным параметром макроса является параметр переменных аргументов, поскольку бессмысленно пытаться отличить, является ли отсутствие аргумента вообще пустым аргументом или отсутствующим аргументом. CPP сохраняет запятую при соответствии определенному стандарту C. В противном случае запятая удаляется как дополнение к стандарту.

Когда DUMMY есть #define DUMMY(x, ...) , __VA_ARGS будет пустым, если DUMMY вызывается только с одним аргументом, который включает в себя оба вызова DUMMY() (один пустой аргумент) и DUMMY(0) (один аргумент, 0 ). Обратите внимание, что стандартный C и C до C 20 не допускали бы этот вызов; они требуют, чтобы был хотя бы один (возможно, пустой) аргумент, соответствующий многоточию. Однако GCC никогда не вводил это ограничение, и GCC опустит запятую с ,##__VA_ARGS__ независимо от --std настройки.

Начиная с C 20, вы можете использовать __VA_OPT__ встроенный макрос как более стандартный способ работы с запятыми (и любыми другими знаками препинания, которые, возможно, потребуется удалить). __VA_OPT__ также позволяет избежать проблемы, представленной выше, с пустыми аргументами, поскольку он использует другой критерий: __VA_OPT__(x) расширяется до, x если __VA_ARGS__ содержит хотя бы один токен; в противном случае он расширяется до пустой последовательности. Следовательно, __VA_OPT__ будет работать так, как ожидалось для макросов в этом вопросе.

Я считаю, что все основные компиляторы сейчас реализуют __VA_OPT__ , по крайней мере, в своих последних версиях.

Комментарии:

1. Почему тогда, когда вы включаете --std=gnu 17 , gcc рассматривает DUMMY() как отсутствие аргументов вместо одного пустого аргумента и удаляет запятую перед ##__VA_ARGS__ ? Смотрите пример компиляции на coliru .

2. @anton_rh: В GCC есть специальное исключение для случая, когда единственный аргумент пуст, а параметр —std указывает gnu. Это в конце документации. Я сделаю пометку в ответе. (Я никогда не замечал этого раньше, потому что я никогда ничего не компилирую с —std=gnu. Извини.)

Ответ №2:

Стандарт __VA_ARGS__ не удаляет конечные , значения при использовании нулевых аргументов. Ваш, ##__VA_ARGS__ который удаляет лишнее , , является расширением GCC.

Это расширение GCC не работает, потому что вы используете стандартный совместимый режим -std=c 17 вместо -std=gnu 17 .

Комментарии:

1. Хорошо, я немного отредактировал код: здесь . Он все еще использует -std=c 17 , но отлично компилируется даже с ##__VA_ARGS__ .

2. @anton_rh Это странно. Интересно, является ли это ошибкой GCC.

3. @user694733: не совсем ошибка. Это ожидаемое (и документированное) поведение для расширения GCC. Смотрите мой ответ.

Ответ №3:

По некоторым причинам (вероятно, это ошибка GCC), если вы используете just #define DUMMY(...) без других аргументов, то ##__VA_ARGS__ не будет работать должным образом (он не удалит запятую, если __VA_ARGS__ она пуста).

Это верно только при компиляции с -std=c 17 . При компиляции с -std=gnu 17 этого не происходит. Но в любом случае ##__VA_ARGS__ это расширение GCC, и код с ##__VA_ARGS__ вообще не должен компилироваться с -std=c 17 . Но GCC позволяет использовать расширения GCC в -std=c 17 режиме, если вы не установите -pedantic флаг. Но, похоже, расширения GCC работают по-разному в режиме -std=c 17 и -std=gnu 17 .

Однако проблему можно обойти:

 #include <utility>

template <typename... TArgs>
void Dummy(const TArgs amp;...args)
{
}

namespace WA
{
    class stub_t {};

    stub_t ArgOrStub()
    {
        return {};
    }

    template <typename T>
    auto ArgOrStub(T amp;amp;t) -> decltype( std::forward<T>(t) )
    {
        return std::forward<T>(t);
    }

    template <typename... TArgs>
    void RemoveStubAndCallDummy(stub_t, TArgs amp;amp;...args)
    {
        Dummy(std::forward<TArgs>(args)...);
    }

    template <typename... TArgs>
    void RemoveStubAndCallDummy(TArgs amp;amp;...args)
    {
        Dummy(std::forward<TArgs>(args)...);
    }
}

#define DUMMY(first, ...) WA::RemoveStubAndCallDummy( WA::ArgOrStub(first), ##__VA_ARGS__ )

int main()
{
    DUMMY();
}
  

При вызове DUMMY() аргумент first будет пустым, и после предварительной обработки мы получим WA::ArgOrStub() который вернет stub_t то, что позже будет удалено при первой перегрузке RemoveStubAndCallDummy . Это громоздко, но я не смог найти лучшего решения.

Ответ №4:

C 20 представил __VA_OPT__ как способ необязательного расширения токенов в переменном макросе, если число аргументов больше нуля.
Это устраняет необходимость в ##__VA_ARGS__ расширении GCC. Если вы можете использовать эту версию стандарта, это должно стать элегантным решением, не зависящим от компилятора.

Последовательность __VA_OPT__(x) , которая допустима только в списке подстановки макроса с переменным аргументом, расширяется до x, если __VA_ARGS__ она непустая, и до nothing, если она пуста.

Таким образом, вы можете просто сделать:
#define DUMMY(...) Dummy("Hello" __VA_OPT__(,) __VA_ARGS__)

Вот хорошая статья в блоге с __VA_OPT__ в действии (и больше о макросах препроцессора):https://www.scs.stanford.edu /~dm/blog/va-opt.html