【问题标题】:C/C++: How should preprocessor directive work on macros argument list?C/C++:预处理器指令应该如何处理宏参数列表?
【发布时间】:2021-02-03 16:47:32
【问题描述】:

C 和 C++ 标准都指定了以下内容:

16.3.1 参数替换 (C++11)

6.10.3.1 参数替换 (C11)

在确定了调用类函数宏的参数后,将进行参数替换。替换列表中的参数,除非前面有 # 或 ## 预处理标记或后跟 ## 预处理标记(见下文),在其中包含的所有宏都已展开后,将替换为相应的参数。在被替换之前,每个参数的预处理标记都被完全宏替换,就好像它们形成了预处理文件的其余部分一样;没有其他可用的预处理令牌。

可以将本段解释为标准要求:

(1)首先识别宏参数(逗号分隔),然后分别展开每个参数中包含的所有宏,

或

(2) 展开参数列表中包含的所有宏,然后识别每个参数。

为了说明这一点,让我们考虑以下示例代码:

#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) 方法一会产生无效代码:

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) 方法 2 生成有效的 C 代码:

int global_a, global_b, global_c;

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

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

对标准的哪些解释是正确的?

【问题讨论】:

  • 我向您展示第三种解释:error: embedding a directive within macro arguments has undefined behavior。这就是 GCC 和 Clang 所做的。该错误发生在宏展开之前。
  • @TedLyngmo 谢谢,完成。
  • @HolyBlackCat 实际上,GCC 9.3.0 正在编译此代码而没有任何抱怨。无论如何,感谢您的评论 - 我改写了标题以更准确。
  • 添加-pedantic-errors,它使GCC更符合标准。
  • (历史记录:某些版本的 GCC,大约 3.0,确实使 UB 的这种特殊情况成为一个硬错误。我不记得它是否曾经以这种方式发布,但我们得到了很多抱怨来自那些对我们破坏他们的代码感到恼火的人......因为他们将#ifdefs 放在printf 的参数中,并且一些C 库将printf 设为宏。所以我们把它降为警告。)

标签: c++ c c-preprocessor standards preprocessor


【解决方案1】:

首先,您根本不能将预处理指令放在类似函数的宏的参数中,因为有些文本与您引用的内容相比有所下降:

如果参数列表中存在预处理标记序列,否则它们将充当预处理指令,则行为未定义。

[N1570, §6.10.3 p11]。

其次,与此无关,标准需要您调用的行为 (1)。这是由您引用的这部分文本指定的:

在被替换之前,每个参数的预处理标记都被完全宏替换,就好像它们形成了预处理文件的其余部分一样;没有其他可用的预处理令牌。

如果在确定参数之间的边界之前扩展类函数宏的参数,这句话就没有任何意义。您也可以通过实验看到这一点,只需稍微修改您的代码:

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

如果CONDITION 未定义或为零,这将正常编译,但当CONDITION 不为零时,您将收到类似以下内容的错误

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

证明GLOBAL_OPT 在寻找FunctionAlias 的四个参数之前没有扩展。

【讨论】:

  • 谢谢,这似乎是答案。有趣的是 gcc -std=c11 (gcc 9.3.0) 应用选项 (2)。
  • @mrm 它肯定不适合我——这是我粘贴在那里的 GCC 的错误消息。
【解决方案2】:

在识别出调用类函数宏的参数后,将进行参数替换。中的一个参数 替换列表,除非前面有 # 或 ## 预处理标记或 后跟 ## 预处理标记(见下文),替换为 在其中包含的所有宏都已被处理后的相应参数 扩大。在被替换之前,每个参数的预处理 标记被完全宏替换,就好像它们形成了其余的 预处理文件;没有其他可用的预处理令牌。

可以将本段解释为标准要求:

(1) 首先识别宏参数(逗号分隔),然后 分别展开每个参数中包含的所有宏,

或

(2) 展开参数列表中包含的所有宏,然后 识别每个参数。

我有点明白你从哪里来,但我并没有真正理解你是如何得出 (2) 作为对文本的合理解释的。您引用的部分首先指定发生参数替换“在识别出调用类似函数的宏的参数后,进行参数替换”,其他所有内容都是描述论据替换需要什么。这包括为实际替换准备的参数,在大多数情况下,通过宏扩展它们。 (1) 是正确的解释。

但请注意,宏扩展是一个迭代过程。粗略地说,宏扩展被重新扫描以进行进一步的宏替换,包括按照引用文本中的描述执行的宏参数的扩展。这就是为什么每个参数的预处理标记都被扩展“就好像它们形成了预处理文件的其余部分一样”很重要的原因。这不仅将每个参数与围绕宿主宏调用的源文本隔离开来,而且最重要的是与其他参数及其扩展隔离开来。

还请注意,如果您发现标准不清楚,您可以自己对此进行测试。你甚至已经生成了你的测试用例。再想一想,你没有提供一个有效的测试用例,因为预处理器指令可能不会出现在宏调用的参数列表中。因此,该标准对预处理示例源的结果没有任何意见。

【讨论】:

  • 问题是我自己做了测试并且 gcc -std=c11 没有给出错误。更重要的是,它应用了方法(2)。
  • 特定编译器以某种方式处理构造的事实并不意味着标准实际需要什么,也不意味着其他编译器——包括同一编译器的未来版本——是否应该被期望表现同样。
  • @mrn,更仔细地查看您的示例代码,我发现 (a) 由于在宏调用的参数列表中包含预处理器指令,它不符合要求,并且 (b)您关于解释(1)和(2)对示例代码的影响的结论似乎将条件包含处理与宏扩展混为一谈。条件包含和宏扩展都是预处理器执行的活动,但既不暗示也不包含另一个。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-12-14
  • 1970-01-01
  • 2010-09-23
  • 1970-01-01
  • 2020-02-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多