Макросы с переменным числом аргументов с нулевыми аргументами не компилируются даже с ## __ VA_ARGS__

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

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

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

int main()
{
    DUMMY();
}

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

g++ -std=c++17 -O3 -Wall main.cpp && ./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 &...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 принимает хотя бы один параметр, а я передаю ноль.


person anton_rh    schedule 04.09.2020    source источник
comment
Я рекомендую вам остановиться после предварительной обработки, чтобы посмотреть, что на самом деле расширяется до макроса.   -  person Some programmer dude    schedule 04.09.2020
comment
@Someprogrammerdude, Dummy("Hello",); :( coliru.   -  person anton_rh    schedule 04.09.2020


Ответы (3)


Важным фактом о макросах 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__, по крайней мере, в своих последних версиях.

person rici    schedule 06.09.2020
comment
Почему тогда, когда вы включаете --std=gnu++17, gcc считает DUMMY() без аргументов вместо одного пустого аргумента и удаляет запятую перед ##__VA_ARGS__? См. Пример компиляции в coliru. - person anton_rh; 07.09.2020
comment
@anton_rh: GCC имеет особое исключение для случая, когда единственный аргумент пуст, а опция --std указывает на gnu. Это в конце документации. Сделаю пометку в ответ. (Я никогда не замечал этого раньше, потому что я никогда ничего не компилирую с --std = gnu. Извините.) - person rici; 07.09.2020

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

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

person user694733    schedule 04.09.2020
comment
Хорошо, я немного отредактировал код: здесь. Он по-прежнему использует -std=c++17, но отлично компилируется даже с ##__VA_ARGS__. - person anton_rh; 04.09.2020
comment
@anton_rh Это странно. Интересно, это ошибка GCC. - person user694733; 04.09.2020
comment
@ user694733: не совсем ошибка. Это ожидаемое (и задокументированное) поведение для расширения GCC. Смотрите мой ответ. - person rici; 06.09.2020

По некоторым причинам (вероятно, это ошибка GCC), если вы используете только #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 &...args)
{
}

namespace WA
{
    class stub_t {};

    stub_t ArgOrStub()
    {
        return {};
    }

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

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

    template <typename... TArgs>
    void RemoveStubAndCallDummy(TArgs &&...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. Это громоздко, но лучшего решения я не нашел.

person anton_rh    schedule 04.09.2020