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

Why does this .c file #include itself?

为什么这个 .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

该文件包含自身,因此可以使用相同的源代码为宏的特定值生成 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

本模块定义的函数有

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

都是由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) {
   ...
}

TEMPLATE2TEMPLATE3 宏在 conf.h 中定义为

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

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

函数模板开头为:

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

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

unsigned char *vsenc8(uint8_t *__restrict in, size_t n, unsigned char *__restrict out) ...

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

unsigned char *vsenc16(uint16_t *__restrict in, size_t n, unsigned char *__restrict out) ...

和另外 2 个内含物定义了 vsenc32vsenc64

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

一个很好的例子是从 QuickJS 中的 atom and opcode 定义生成枚举、字符串和结构数组。

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

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

是什么让这个特定的例子有点难以理解,因为实现文件被非常规地解析了 5 次,首先有一些准备定义,然后是两个函数的四个变体。第一遍(在 #ifndef USIZE 预处理器块内)将定义所需的宏和 non-variant 内容,并将递归 #include 本身四次,具有不同的 USIZE 值(8, 16, 32, 64) 作为模板值。当递归包含时,相应的#else预处理器块被解析为根据用于pass的USIZE宏常量的值生成的两个函数的结果。

更传统、概念上更清晰、更容易理解的方法是包含来自不同文件的模板函数,比如 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"

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

以这种方式递归和反复包含自身会产生一种 hocus-pocery 的感觉,这会为混淆的 C 竞赛项目赢得掌声,但不会为关键任务生产代码赢得掌声。

是递归。递归在这里很有用,因为 C 预处理没有循环。此外,最好是使用一个文件而不是扩散多个文件来实施技巧。

假设您需要编写一个函数,将 1 到 5 的整数插入到模板字符串中,并将其打印在标准输出上。假设您被要求只编写一个函数并且被禁止使用循环或 copy-pasted printf 语句。你可以这样做:

void template_print(const char *fmt, int n)
{
   if (n == 0) {
     template_print(fmt, 1);
     template_print(fmt, 2);
     template_print(fmt, 3);
     template_print(fmt, 4);
     template_print(fmt, 5);
   } else {
     /* imagine there are 30 lines of statements here we don't want
        to repeat five times. */
     printf(fmt, n);
   }
}

然后 top-level 对它的调用 template_print("whatever %d\n", 0) 通过 n 参数的零参数来区分。

带0的top-level调用就像vsimple.c的初始处理,没有定义USIZE

对一个功能的要求类似于需要生成单个 self-contained .c 文件,而不是 #include 实现的“接口”文件。