1
votes

Both C and C++ standards specify the following:

16.3.1 Argument substitution (C++11)

6.10.3.1 Argument substitution (C11)

After the arguments for the invocation of a function-like macro have been identified, argument substitution takes place. A parameter in the replacement list, unless preceded by a # or ## preprocessing token or followed by a ## preprocessing token (see below), is replaced by the corresponding argument after all macros contained therein have been expanded. Before being substituted, each argument’s preprocessing tokens are completely macro replaced as if they formed the rest of the preprocessing file; no other preprocessing tokens are available.

One can interpret this paragraph as if the standards require to:

(1) At first identify macro arguments (comma separated) and then expand all the macros contained in each argument separately,

or

(2) Expand all the macros contained in the argument list and then identify each argument.

To illustrate this, let's consider this sample code:

#define CONDITION (0)

#if (CONDITION > 0)
#define FunctionAlias(par_a, par_b, par_opt, par_c) \
          FunctionName(par_a, par_b, par_opt, par_c)
#else
#define FunctionAlias(par_a, par_b, par_c) \
          FunctionName(par_a, par_b, par_c)
#endif

int global_a, global_b, global_c;
#if (CONDITION > 0)
int global_opt;
#endif

void FunctionName(int a, int b, int c)
{
}
 
void AnotherFunction()
{
   FunctionAlias(
                  global_a,
                  global_b,
                  #if (CONDITION > 0)
                  global_opt,
                  #endif
                  global_c
                );
}

(1) Approach one would produce invalid code:

int global_a, global_b, global_c;

void FunctionName(int a, int b, int c)
{
}

void AnotherFunction()
{
  FunctionName(global_a, global_b, #if ((0) > 0) global_opt);
}

(2) Approach 2 produces valid C code:

int global_a, global_b, global_c;

void FunctionName(int a, int b, int c)
{
}

void AnotherFunction()
{
   FunctionName(global_a, global_b, global_c);
}

Which of the interpretations of the standards is the correct one?

2
I present to you a third interpretation: error: embedding a directive within macro arguments has undefined behavior. This is what GCC & Clang do. The error happens before the macro is expanded.HolyBlackCat
@TedLyngmo Thanks, done.mrn
@HolyBlackCat Actually, GCC 9.3.0 is compiling this code without a complain. Anyway, thanks for your comment - I rephrased the title to be more accurate.mrn
Add -pedantic-errors, it makes GCC more standard-conformant.HolyBlackCat
(Historical note: Some versions of GCC, circa 3.0, did make this particular case of UB a hard error. I don't remember if it ever made it into a release that way, but we got so many complaints from people who were annoyed at us breaking their code ... because they were putting #ifdefs inside the arguments to printf, and some C libraries make printf a macro. So we backed it down to a warning.)zwol

2 Answers

4
votes

First, you cannot, at all, put preprocessing directives inside the arguments to a function-like macro, because of some text a little down from what you quoted:

If there are sequences of preprocessing tokens within the list of arguments that would otherwise act as preprocessing directives, the behavior is undefined.

[N1570, §6.10.3 p11].

Second, independent of that, the standard requires the behavior you called (1). This is specified by this part of the text you quoted:

Before being substituted, each argument’s preprocessing tokens are completely macro replaced as if they formed the rest of the preprocessing file; no other preprocessing tokens are available.

This sentence wouldn't make any sense if the arguments to a function-like macro were expanded before the boundaries between arguments were identified. You can also see this by experiment, with a slight modification of your code:

#if (CONDITION > 0)
#define FunctionAlias(par_a, par_b, par_opt, par_c) \
          FunctionName(par_a, par_b, par_opt, par_c)
#else
#define FunctionAlias(par_a, par_b, par_c) \
          FunctionName(par_a, par_b, par_c)
#endif

int global_a, global_b, global_c;
#if (CONDITION > 0)
int global_opt;
#define GLOBAL_OPT global_opt,
#else
#define GLOBAL_OPT /*nothing*/
#endif

void FunctionName(int a, int b, 
#if CONDITION > 0
                  int opt,
#endif
                  int c)
{
}
 
void AnotherFunction()
{
   FunctionAlias(
                  global_a,
                  global_b,
                  GLOBAL_OPT
                  global_c
                );
}

This will compile fine if CONDITION is not defined or zero, but when CONDITION is nonzero, you will get an error along the lines of

test.c: In function ‘AnotherFunction’:
test.c:28:17: error: macro "FunctionAlias" requires 4 arguments, but only 3 given
   28 |                 );
      |                 ^

demonstrating that GLOBAL_OPT was not expanded before looking for the four arguments to FunctionAlias.

1
votes

After the arguments for the invocation of a function-like macro have been identified, argument substitution takes place. A parameter in the replacement list, unless preceded by a # or ## preprocessing token or followed by a ## preprocessing token (see below), is replaced by the corresponding argument after all macros contained therein have been expanded. Before being substituted, each argument’s preprocessing tokens are completely macro replaced as if they formed the rest of the preprocessing file; no other preprocessing tokens are available.

One can interpret this paragraph as if the standard required to:

(1) At first identify macro arguments (comma separated) and then expand all the macros contained in each argument separately,

or

(2) Expand all the macros contained in the argument list and then identify each argument.

I sort of see where you're coming from, but I don't really follow how you arrive at (2) as a plausible interpretation of the text. The section you've quoted starts by specifying that argument substitution takes place "After the arguments for the invocation of a function-like macro have been identified, argument substitution takes place", and everything else is a description of what argument substitution entails. That includes the arguments being prepared for actual substitution by, under most circumstances, macro-expanding them. (1) is the correct interpretation.

Do note, however, that macro expansion is an iterative process. Speaking roughly, macro expansions are rescanned for further macro replacement, including the expansions of macro arguments performed as described in the quoted text. This is why it is important that each argument's preprocessing tokens are expanded "as if they formed the rest of the preprocessing file". That isolates each one not just from the source text surrounding the host macro invocation but most importantly from the other arguments and their expansions.

Note also that you could have tested this for yourself if you found the standard unclear. You even produced your test case already. On second thought, you did not provide a valid test case, because preprocessor directives may not appear in the argument list of a macro invocation. The standard therefore has nothing to say about the result of preprocessing your example source.