4

Inspired by some answers from this question, I've got the following C++ code that tries to emulate default function arguments with macros.

// do_func(int a, int b, int c = 0);
void do_func(int, int, int);

#define FUNC_2_ARGS(a, b)    do_func(a, b, 0)
#define FUNC_3_ARGS(a, b, c) do_func(a, b, c)

#define GET_4TH_ARG(arg1, arg2, arg3, arg4, ...) arg4
#define MACRO_CHOOSER(...) \
    GET_4TH_ARG(__VA_ARGS__, FUNC_3_ARGS, \
                FUNC_2_ARGS, not_func)

#define func(...) MACRO_CHOOSER(__VA_ARGS__)(__VA_ARGS__)

func(1, 2)
func(1, 2, 3)

The idea being that MACRO_CHOOSER(__VA_ARGS__) should expand to either GET_4TH_ARG(1, 2, 3, FUNC_3_ARGS, FUNC_2_ARGS, not_func which further expands to FUNC_2_ARGS which now takes (1, 2) and results into do_func(1, 2, 0). Similarly, if three arguments are passed, FUNC_3_ARGS is chosen.

It works perfectly well with both gcc and clang when run like g++ -E x.cpp:

# 0 "x.cpp"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "x.cpp"
void do_func(int, int, int);
# 13 "x.cpp"
do_func(1, 2, 0)
do_func(1, 2, 3)

However, Visual C++ 19.32's seems to apply GET_4TH_ARG before expanding __VA_ARGS__ and always gets the following result when run with cl /P x.cpp:

#line 1 "x.cpp"
void do_func(int, int, int);
// snip
not_func(1, 2)
not_func(1, 2, 3)

What behavior is correct here according to C++ or C standard? Is it even a legal C++? Or maybe it's legal C, but not C++?

Of course, there are better macros and BOOST_PP, but I'm wondering what's the logic here.

yeputons
  • 8,478
  • 34
  • 67
  • 12
    What does [`/Zc:preprocessor`](https://learn.microsoft.com/en-us/cpp/build/reference/zc-preprocessor?view=msvc-170) give you? MSVC's preprocessor is non-conforming to the standard(s) by default. – user17732522 May 07 '23 at 11:28
  • 1
    @user17732522 Wonderful, that brings them together, thank you. Feel free to write that as an answer. – yeputons May 07 '23 at 11:35
  • @yeputons the [language-lawyer] tag might make a quick answer very difficult. – Richard Critten May 07 '23 at 11:52
  • 1
    It's nice that MS now agrees that the default behavior of their preprocessor does not conform. [They didn't always](https://stackoverflow.com/a/7459803). – John Bollinger May 07 '23 at 13:57
  • 1
    Microsoft understands the term `single item` (C++, N4713) as a "single, [permanently indivisible preprocessor token](https://stackoverflow.com/a/7459803/1778275)" (which leads to issues while porting code from gcc/clang/other, which has different understanding). – pmor May 11 '23 at 15:51

1 Answers1

4

Why does that macro-emulated function default argument expand differently in GCC and MSVC (C or C++)?

Because one of those implementations does it incorrectly. Guess which.

What behavior is correct here according to C++ or C standard? Is it even a legal C++? Or maybe it's legal C, but not C++?

As far as I know or can determine, current C and C++ specify the same preprocessor behavior with respect to all relevant aspects of macro expansion, and they have been so aligned at least since their respective 2011 revisions. In the C++ specification, these are covered by cpp.replace:

If there is a ... immediately preceding the ) in the function-like macro definition, then the trailing arguments (if any), including any separating comma preprocessing tokens, are merged to form a single item: the variable arguments. The number of arguments so combined is such that, following merger, the number of arguments is either equal to or one more than the number of parameters in the macro definition (excluding the ...).

, cpp.subst:

[...] the preprocessing tokens naming the parameter are replaced by a token sequence determined as [...] the preprocessing tokens of corresponding argument after all macros contained therein have been expanded. [...]

An identifier __VA_ARGS__ that occurs in the replacement list shall be treated as if it were a parameter, and the variable arguments shall form the preprocessing tokens used to replace it.

, and cpp.rescan:

After all parameters in the replacement list have been substituted [...] the resulting preprocessing token sequence is rescanned, along with all subsequent preprocessing tokens of the source file, for more macro names to replace.

Let's consider your macro stack, and your two-arg example invocation:

func(1, 2)

With the given definition of func, the variable arguments are 1, 2, and applying macro expansion to that does not change it. The first expansion of the macro invocation thus produces this:

MACRO_CHOOSER(1, 2)(1, 2)

, which is subject to re-scan. But there's in invisible difference there between what GCC does and what MSVC does. To GCC, each 1, 2 comprises three preprocessing tokens, whereas to MSVC, the whole thing has become a single one. This is a difference in the interpretation of "merged to form a single item" in cpp.replace. I think you will be able to follow how MSVC's interpretation leads to the final outcome you observed.

GCC is right, and Microsoft now acknowledges this, as you can recognize by the fact that turning on its /Zc:preprocessor switch to obtain standard-conforming preprocessing causes it to preprocess the example the same way GCC does.

As far as the text of the specification goes, you can infer from the fact that cpp.subst says "the variable arguments shall form the preprocessing tokens used to replace it" (emphasis added; note that "tokens" is plural) that forming a single "item" of them is just about temporary grouping. It is not meant to imply that such grouping prevents the erstwhile variable arguments from being interpreted as separate preprocessor tokens during the subsequent rescan.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157