2
votes

If I try to compile the following code:

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

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

int main()
{
    DUMMY();
}

I get the following compilation error:

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

The code compiles fine when I add at least one parameter:

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

But I'm not sure that it is correct, because DUMMY takes at least one parameter, but I pass zero.

3
I recommend that you stop after preprocessing to see what the macros actually expands to. - Some programmer dude
@Someprogrammerdude, Dummy("Hello",); :( coliru. - anton_rh

3 Answers

2
votes

An important fact about C/C++ macros is that it is impossible to invoke them with no parameters, because a macro parameter is allowed to be an empty token sequence.

Consequently, DUMMY() invokes the macro DUMMY with a single empty parameter, not with zero parameters. This explains why the second example works, and it also explains why the first example produces a syntax error.

The GCC extension deletes the comma from , ##__VA_ARGS__ when __VA_ARGS__ has no elements. But a single empty argument is not the same as no arguments. When you define DUMMY as #define DUMMY(...) , you are guaranteeing that __VA_ARGS__ has at least one argument, so the , won't be deleted.

***Note: GCC does make an exception to that rule if you don't specify some ISO standard with the --std option. In that case, if ... is the only macro parameter and the invocation has an empty argument, then ,##__VA_ARGS__ does drop the comma. This is noted in the CPP manual in the Variadic Marcos section:

The above explanation is ambiguous about the case where the only macro parameter is a variable arguments parameter, as it is meaningless to try to distinguish whether no argument at all is an empty argument or a missing argument. CPP retains the comma when conforming to a specific C standard. Otherwise the comma is dropped as an extension to the standard.

When DUMMY is #define DUMMY(x, ...), __VA_ARGS will be empty if DUMMY is invoked with only one argument, which includes both the invocations DUMMY() (one empty argument) and DUMMY(0) (one argument, 0). Note that standard C, and C++ until C++20, would not allow this invocation; they require that there be at least one (possibly empty) argument corresponding to the ellipsis. GCC never imposed this restriction, though, and GCC will omit the comma with ,##__VA_ARGS__ regardless of the --std setting.

Starting with C++20, you can use the __VA_OPT__ built-in macro as a more standard way of dealing with commas (and any other punctuation which might need to be deleted). __VA_OPT__ also avoids the problem presented above with empty arguments, because it uses a different criterion: __VA_OPT__(x) expands to x if __VA_ARGS__ contains at least one token; otherwise, it expands to an empty sequence. Consequently, __VA_OPT__ would work as expected for the macros in this question.

I believe that all major compilers now implement __VA_OPT__, at least in their recent versions.

2
votes

Standard __VA_ARGS__ doesn't remove trailing , when using zero arguments. Your ##__VA_ARGS__ which removes extra , is an GCC extension.

This GCC extension doesn't work because you are using standard compatible mode -std=c++17, instead of -std=gnu++17.

0
votes

For some reasons (it's probably a GCC bug), if you use just #define DUMMY(...) without other arguments, then ##__VA_ARGS__ won't work as expected (it will not remove comma if __VA_ARGS__ is empty).

This is true only when you compile with -std=c++17. When you compile with -std=gnu++17, this doesn't happen. But in any case ##__VA_ARGS__ is GCC extension, and the code with ##__VA_ARGS__ must not be compilable with -std=c++17 at all. But GCC allows to use GCC extensions in -std=c++17 mode unless you set -pedantic flag. But it seems that GCC extensions work differently in -std=c++17 and -std=gnu++17 mode.

The problem however can be worked around:

#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();
}

When you call DUMMY() the first argument will be empty, and after preprocessing we will get WA::ArgOrStub() which will return stub_t that will later be removed by first overload of RemoveStubAndCallDummy. It's bulky but I couldn't fine better solution.