3

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.

anton_rh
  • 8,226
  • 7
  • 45
  • 73

4 Answers4

3

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.

user694733
  • 15,208
  • 2
  • 42
  • 68
  • Ok, i edited code a little: [here](http://coliru.stacked-crooked.com/a/80636d57fee0ee38). It still uses `-std=c++17` but compiles fine even with `##__VA_ARGS__`. – anton_rh Sep 04 '20 at 11:35
  • 1
    @anton_rh That is weird. I wonder if that is GCC bug. – user694733 Sep 04 '20 at 11:45
  • 1
    @user694733: not exactly a bug. It's expected (and documented) behaviour for the GCC extension. See my answer. – rici Sep 06 '20 at 19:22
3

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.

rici
  • 234,347
  • 28
  • 237
  • 341
  • Why then, when you enable `--std=gnu++17`, _gcc_ considers `DUMMY()` as no arguments instead of one empty argument, and removes comma before `##__VA_ARGS__`? See compilation example on [coliru](https://coliru.stacked-crooked.com/a/78e71e0cf4fa5ab3). – anton_rh Sep 07 '20 at 03:55
  • @anton_rh: GCC has a specific exception for the case where the only argument is empty and the --std option indicates gnu. It's at the end of the documentation. I'll make a note in the answer. (I'd never noticed that before because I never compile with --std=gnu anything. Sorry.) – rici Sep 07 '20 at 05:11
1

C++20 introduced __VA_OPT__ as a way to optionally expand tokens in a variadic macro if the number of arguments is greater than zero.
That removes the need for the ##__VA_ARGS__ GCC extension. If you can use that version of the standard, that should make for an elegant, compiler-unspecific solution.

The sequence __VA_OPT__(x), which is only legal in the substitution list of a variable-argument macro, expands to x if __VA_ARGS__ is non-empty and to nothing if it is empty.

So you can simply do:
#define DUMMY(...) Dummy("Hello" __VA_OPT__(,) __VA_ARGS__)

Here is good blog article with __VA_OPT__ in action (and more on preprocessor macros): https://www.scs.stanford.edu/~dm/blog/va-opt.html

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
gg99
  • 445
  • 6
  • 12
0

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.

anton_rh
  • 8,226
  • 7
  • 45
  • 73