为什么这个 .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) {
...
}
TEMPLATE2
和 TEMPLATE3
宏在 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_)
这些宏是经典的预处理器结构,用于通过标记粘贴创建标识符。 TEMPLATE2
和 TEMPLATE2_
通常称为 GLUE
和 XGLUE
。
函数模板开头为:
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 个内含物定义了 vsenc32
和 vsenc64
。
预处理源代码的这种用法在单独的文件中更常见:一个用于实例化部分,具有所有通用定义,尤其是宏,另一个文件用于代码和数据模板,多次包含具有不同的宏定义。
@chqrlie 接受的答案 100% 解释了正在发生的事情。这只是一个补充评论。
如果使用 C++,我们可以定义两个模板函数来提供 vsenc8
、vsenc16
、vsenc32
、vsenc64
和 vsdec8
的所有实现, vsdec16
、vsdec32
、vsdec64
。然而相比之下,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
实现的“接口”文件。
为什么这个 .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) {
...
}
TEMPLATE2
和 TEMPLATE3
宏在 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_)
这些宏是经典的预处理器结构,用于通过标记粘贴创建标识符。 TEMPLATE2
和 TEMPLATE2_
通常称为 GLUE
和 XGLUE
。
函数模板开头为:
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 个内含物定义了 vsenc32
和 vsenc64
。
预处理源代码的这种用法在单独的文件中更常见:一个用于实例化部分,具有所有通用定义,尤其是宏,另一个文件用于代码和数据模板,多次包含具有不同的宏定义。
@chqrlie 接受的答案 100% 解释了正在发生的事情。这只是一个补充评论。
如果使用 C++,我们可以定义两个模板函数来提供 vsenc8
、vsenc16
、vsenc32
、vsenc64
和 vsdec8
的所有实现, vsdec16
、vsdec32
、vsdec64
。然而相比之下,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
实现的“接口”文件。