为什么在GCC和MSVC(C或C++)中,宏模拟的函数默认参数展开方式不同?

huangapple go评论62阅读模式
英文:

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

问题

以下是您要翻译的内容:

受到this question中的一些答案的启发,我有以下试图使用宏模拟默认函数参数的C++代码。

// 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)

这个想法是MACRO_CHOOSER(__VA_ARGS__)应该展开为GET_4TH_ARG(1, 2, 3, FUNC_3_ARGS, FUNC_2_ARGS, not_func),进一步展开为FUNC_2_ARGS,然后接受(1, 2)并产生do_func(1, 2, 0)。同样,如果传递了三个参数,将选择FUNC_3_ARGS

当以g++ -E x.cpp运行时,它在gcc和clang中都能完美工作:

# 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)

然而,当使用cl /P x.cpp运行时,Visual C++ 19.32似乎在展开__VA_ARGS__之前应用了GET_4TH_ARG,并且始终得到以下结果:

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

根据C++或C标准,哪种行为是正确的?这是否甚至是合法的C++?或者也许它是合法的C,但不是C++?

当涉及到C++宏展开和参数的处理时,不同的编译器可能表现不同,这取决于它们的实现细节和规则。根据C++标准,具体宏展开的行为没有明确定义,因此这种行为可能会因编译器而异。

通常来说,编译器可能会在展开__VA_ARGS__之前处理宏参数,这可能导致不同编译器的行为差异。这个问题似乎是在宏展开和参数处理方面的编译器特定行为,而不是C++或C标准的问题。

因此,为了编写可移植的代码,最好避免依赖于这种宏展开的细节行为,尤其是在不同编译器之间。如您所提到的,有更好的宏和工具,如BOOST_PP,可以更可靠地实现所需的功能。

英文:

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.

答案1

得分: 4

The macro expansion behavior differs between GCC and MSVC due to their interpretation of "merged to form a single item." GCC considers 1, 2 as three preprocessing tokens, while MSVC treats it as a single token. GCC's interpretation aligns with the C++ specification, and Microsoft now acknowledges this by offering a /Zc:preprocessor switch for standard-conforming preprocessing. According to the specification, the variable arguments are grouped temporarily, but this grouping doesn't prevent them from being treated as separate tokens during subsequent rescanning.

英文:

> 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.

huangapple
  • 本文由 发表于 2023年5月7日 19:21:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/76193609.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定