为什么这个 .c 文件 #include 本身?

Rod*_*lli 53 c c-preprocessor

为什么这个.c文件#include本身?

vsimple.c

#define USIZE 8
#include "vsimple.c"
#undef USIZE

#define USIZE 16
#include "vsimple.c"
#undef USIZE

#define USIZE 32
#include "vsimple.c"
#undef USIZE

#define USIZE 64
#include "vsimple.c"
#undef USIZE
Run Code Online (Sandbox Code Playgroud)

chq*_*lie 85

该文件包含自身,因此可以使用相同的源代码为宏的特定值生成 4 组不同的函数USIZE

这些#include指令实际上包含在 中#ifndef,这将递归限制为单个级别:

#ifndef USIZE

// common definitions
...
//

#define VSENC vsenc
#define VSDEC vsdec

#define USIZE 8
#include "vsimple.c"
#undef USIZE

#define USIZE 16
#include "vsimple.c"
#undef USIZE

#define USIZE 32
#include "vsimple.c"
#undef USIZE

#define USIZE 64
#include "vsimple.c"
#undef USIZE

#else // defined(USIZE)

// macro expanded size specific functions using token pasting

...

#define uint_t TEMPLATE3(uint, USIZE, _t)

unsigned char *TEMPLATE2(VSENC, USIZE)(uint_t *__restrict in, size_t n, unsigned char *__restrict out) {
   ...
}

unsigned char *TEMPLATE2(VSDEC, USIZE)(unsigned char *__restrict ip, size_t n, uint_t *__restrict op) {
   ...
}

#endif
Run Code Online (Sandbox Code Playgroud)

该模块中定义的函数有

// vsencNN: compress array with n unsigned (NN bits in[n]) values to the buffer out. Return value = end of compressed output buffer out
unsigned char *vsenc8( unsigned char  *__restrict in, size_t n, unsigned char  *__restrict out);
unsigned char *vsenc16(unsigned short *__restrict in, size_t n, unsigned char  *__restrict out);
unsigned char *vsenc32(unsigned       *__restrict in, size_t n, unsigned char  *__restrict out);
unsigned char *vsenc64(uint64_t       *__restrict in, size_t n, unsigned char  *__restrict out);

// vsdecNN: decompress buffer into an array of n unsigned values. Return value = end of compressed input buffer in
unsigned char *vsdec8( unsigned char  *__restrict in, size_t n, unsigned char  *__restrict out);
unsigned char *vsdec16(unsigned char  *__restrict in, size_t n, unsigned short *__restrict out);
unsigned char *vsdec32(unsigned char  *__restrict in, size_t n, unsigned       *__restrict out);
unsigned char *vsdec64(unsigned char  *__restrict in, size_t n, uint64_t       *__restrict out);
Run Code Online (Sandbox Code Playgroud)

它们都是从vsimple.c中的两个函数定义扩展而来:

unsigned char *TEMPLATE2(VSENC, USIZE)(uint_t *__restrict in, size_t n, unsigned char *__restrict out) {
   ...
}

unsigned char *TEMPLATE2(VSDEC, USIZE)(unsigned char *__restrict ip, size_t n, uint_t *__restrict op) {
   ...
}
Run Code Online (Sandbox Code Playgroud)

和宏在conf.hTEMPLATE2TEMPLATE3定义为

#define TEMPLATE2_(_x_, _y_) _x_##_y_
#define TEMPLATE2(_x_, _y_) TEMPLATE2_(_x_,_y_)

#define TEMPLATE3_(_x_,_y_,_z_) _x_##_y_##_z_
#define TEMPLATE3(_x_,_y_,_z_) TEMPLATE3_(_x_, _y_, _z_)
Run Code Online (Sandbox Code Playgroud)

这些宏是经典的预处理器结构,用于通过标记粘贴创建标识符。TEMPLATE2TEMPLATE2_更常被称为GLUEXGLUE

函数模板开始为:

unsigned char *TEMPLATE2(VSENC, USIZE)(uint_t *__restrict in, size_t n, unsigned char *__restrict out) ...
Run Code Online (Sandbox Code Playgroud)

它在第一个递归包含中被扩展,USIZE定义为8

unsigned char *vsenc8(uint8_t *__restrict in, size_t n, unsigned char *__restrict out) ...
Run Code Online (Sandbox Code Playgroud)

第二个递归包含USIZE定义为16,将模板扩展为:

unsigned char *vsenc16(uint16_t *__restrict in, size_t n, unsigned char *__restrict out) ...
Run Code Online (Sandbox Code Playgroud)

还有 2 个包含体定义vsenc32vsenc64

这种预处理源代码的用法在单独的文件中更为常见:一个用于实例化部分,具有所有通用定义,尤其是宏,另一个文件用于代码和数据模板,该文件多次包含不同的宏定义。

一个很好的例子是从 QuickJS 中的原子操作码定义生成枚举、字符串和结构数组。

  • @JacobManaker:有趣的方法......但是这种无界递归受到严格限制,不会调用未定义的行为,正如 C 标准在 **5.2.4.1 翻译限制** 中所记录的那样 *该实现应能够翻译和执行至少一个程序至少包含以下每一项限制的一个实例:* [...] *- `#included` 文件的 15 个嵌套级别*。 (9认同)
  • 这种技术[有时被称为“X-macros”](https://quuxplusone.github.io/blog/2021/02/01/x-macros/),尽管我个人觉得这个名字很糟糕。 (7认同)

Foo*_*ooF 41

@chqrlie 接受的答案 100% 解释了正在发生的事情。这只是一个补充性的评论。

vsenc8如果使用 C++,我们可以定义两个模板函数来提供, vsenc16, vsenc32,vsenc64vsdec8, vsdec16, vsdec32,的所有实现vsdec64。但相比之下,C 是一种非常简单的语言,并且不支持模板。拥有相同功能(在更丑陋的包装中)的一个常见技巧是使用该语言的哑宏功能,并让 C 预处理器为我们完成同等的工作。大多数具有一定经验的 C 程序员在其职业生涯中都会反复遇到并使用这种构造。

这个特定示例理解起来有点乏味,因为实现文件被非常规地解析了 5 次,首先有一些准备定义,然后是两个函数的四个变体。第一遍(在#ifndef USIZE预处理器块内)将定义所需的宏和非变量内容,并将使用不同的值(、、、 )作为模板值自行递归#include四次。当递归包含时,相应的预处理器块将根据用于传递的宏常量的值生成的两个函数的结果进行解析。USIZE8163264#elseUSIZE

更传统、概念上更清晰且易于理解的方法是包含来自不同文件的模板函数,例如vsimple.impl

#define USIZE 8
/* Generate vsenc8(), vsdec8()... */ 
#include "vsimple.impl"

#undef USIZE
#define USIZE 16
/* Generate vsenc16(), vsdec16()... */ 
#include "vsimple.impl"

#undef USIZE
#define USIZE 32
/* Generate vsenc32(), vsdec32()... */ 
#include "vsimple.impl"

#undef USIZE
#define USIZE 64
/* Generate vsenc64(), vsdec64()... */ 
#include "vsimple.impl"
Run Code Online (Sandbox Code Playgroud)

然后,还可以将包含文件vsimple.c和被包含文件vsimple.impl组织得更加清楚它们定义的内容和时间。大多数 C 程序员都会识别实现模式并立即知道发生了什么。

以这种方式递归地、重复地包含自身会让人产生一种魔术般的感觉,这会为模糊的 C 竞赛条目赢得掌声,但不会为关键任务的生产代码赢得掌声。

  • 好建议。顺便说一句,`<stdio.h>`、`<stdlib.h>`和`"vint.h"`包含在`#ifdef`文件的错误部分中,由于包含保护,这不是问题,但仍然很草率。 (3认同)
  • @chqrlie:我只是基本上阐述并表达了您在回答中已经提出的观点,“预处理源代码的这种用法在单独的文件中更常见......”;-) (2认同)
  • 我的假设是,这样做的目的不是为了混淆,而是为了减少将此代码复制到其他项目时需要管理的文件数量。 (2认同)
  • @MosheKatz 我并不是说最初的目的是混淆。我只是想表达,在移植国际 C 编程竞赛 (https://www.ioccc.org/) 中的参赛作品时,递归 #include myself 可能是一个有用的补充。在我看来,避免将文件添加到三十个或更多文件的列表中几乎不值得付出代码可读性的代价。 (2认同)