本文是「在 C 的世界以外」这篇文章的一个大的背景。前端
假设在一个名曰 foo
的函数的内部须要计算点距:正则表达式
void foo(void *x, void *y, ...) { ... 略去若干行 ... /* 此处是计算点距的代码,暂时不知如何写*/ ... 略去若干行 ... }
foo
所接受的两个参数 x
与 y
分别是指向两个点对象的指针。为了追求通用性,foo
这个函数努力成为一个不依赖具体数据类型的「泛型」函数。也就是说,如今咱们并不知道 x
与 y
所指向的点对象究竟有着怎样的数据结构。也许它们的数据结构是:编程
typedef double * Point;
也多是:segmentfault
typedef struct { double *coord; size_t n; } Point;
还有多是:数组
typedef struct { char **coord; size_t n; void *attachment; } Point;
没错,点的坐标也能够是字符串数组啊!数据结构
总之,foo
所面对的「点」对象,变化万千。这种状况下,如何在 foo
函数中计算 x
与 y
这两个点的距离?less
若是用 C++,这个问题很容易解决,例如:编程语言
template<typename T, typename D> void foo(T &x, T &y) { ... 略去若干行 ... D d = compute_distance<T, D>(x, y); ... 略去若干行 ... }
个人 C++ 知识还停留在 10 年以前,并且也一直都没怎么用过,不知道这么写是否是正确。在上述代码中,T
表示点的类型,D
表示点的「坐标份量」的类型。这里假设点是一个各向同性空间中的点,也就是说,点的各维坐标份量的数据类型都是 D
。若是不做这一假设,代码彷佛无法写。编辑器
若是坚持用 C 来写 foo
,该怎么作?只能靠函数指针了。看下面的代码:函数
void foo(void *x, void *y, void * (*compute_distance)(void *, void *)) { ... 略去若干行 ... void *d = compute_distance(x, y); ... 略去若干行 ... /* 不要忘记释放 d */ free(d); }
这样来看,C 也不是那么不堪。只要在 compute_distance
中完成 x
到 y
的距离的计算,而后将结果经过堆空间传递给 foo
函数。
如今,继续增长 foo
的需求。之因此要计算两个点之间的距离,确定是用来比较远近用的。可让 foo
从 x
与 y
中选出距离点 z
更近的那个点,并将其返回。这一需求,用 C++ 可描述为:
template<typename T, typename D> T & foo(T &x, T &y, T &z) { D d_xz = compute_distance<T, D>(x, z); D d_yz = compute_distance<T, D>(y, z); return d_xz < d_yz ? x : y; }
若是用 C 来写,写到三目运算符那行代码时,发现无法写了:
void * foo(void *x, void *y, void *z, void * (*compute_distance)(void *, void *)) { void *d_xz = compute_distance(x, z); void *d_yz = compute_distance(y, z); void *ret = ____无法写了___ ? x : y; free(d_yz); free(d_xz); return ret; }
怎么比较 d_xz
与 d_yz
呢?咱们并不知道它们的类型,甚至都不能肯定它们是否能参与 <
运算。C++ 没这个问题,由于在 C++ 中,咱们能够对类型为 D
的对象进行 <
运算符重载。
没有办法,只好再向 foo
函数提供一个 C 标准库中的 qsort
风格的函数指针:
void * foo(void *x, void *y, void *z, void * (*compute_distance)(void *, void *), int (*cmp)(void *, void *)) { void *d_xz = compute_distance(x, z); void *d_yz = compute_distance(y, z); void *ret = cmp(d_xz, d_yz) < 0 ? x : y; free(d_yz); free(d_xz); return ret; }
若是 foo
继续复杂下去,C 版本的 foo
函数也许会变成函数指针大本营。譬如,怎么进行 d_xz
与 d_yz
的四则运算,怎么计算它们的绝对值,怎么对它们进行开方……
即便这些都不是问题,最终咱们可以忍受 foo
函数变成了函数指针的乱炖菜,可是它的性能会比 C++ 版本的 foo
函数差许多。由于 C++ 模板代码会被编译器编译为针对特定数据类型的代码,它的 d_xz
与 d_yz
位于栈空间,而且还具备内联函数的优点。C 版本的 foo
函数的境况则很是悲惨,它须要频繁的调用一组外部函数来完成一些很是简单的运算,并且 d_xz
与 d_yz
须要堆空间的分配与释放操做。也许 C 标准库中的 qsort
就是这样败给 C++ STL 中的 sort
函数的。
C 不适合这种细微的抽象,特别是那些可能须要用于 CPU 密集的数值运算的程序的函数不适合这样细微的抽象。像数组、链表、树、图这些基本的数据结构,能够用 void *
表示所存储的数据,可是这些数据结构自己不须要去作依赖于数据类型的运算,因此能够对它们进行抽象。若是是写针对于人类平常活动的一些程序,譬如写 GUI 库,写一些桌面软件——文本编辑器、阅读器、文件管理器之类,怎么抽象都没大有关系。不过,要写 GNU Science Library 这样的库,就不能去抽象了。GSL 库几乎为每一种基本的数据类型都提供了一套代码。
C 的用武之地是写面向特定需求的代码。若是需求变了,代码再从新写一遍……这样说,彷佛很落伍,可是目前作前端的不是在重写过去的桌面程序代码么?作手机 APP 的,不是在重写过去的那些桌面程序代码么?代码重写,增长就业岗位,拯救颓废的世界经济……有什么很差?
单靠 C 是难以挣脱地心引力的。在犹豫是否投靠 C++ 之时,我想起了曾经玩了一段时间的 m4。
m4 说,你能够这样写啊!
void * foo(void *x, void *y, void *z) { DISTANCE_TYPE d_xz, d_yz; compute_distance_T(x, z, d_xz); compute_distance_T(y, z, d_yz); return less_than_T(d_xz, d_yz) ? x : y; }
代码中的 DISTANCE_TYPE
、compute_distance_T
以及 less_than_T
皆为宏。它们能够是 C 的宏,也能够是 m4 的宏。若是它们是 C 的宏,那么 foo
的定义须要放在头文件中供其余模块使用,这样最终的结果就相似于未经编译器优化的 C++ 模板代码的最终结果。若是它们是 m4 的宏,那么 foo
的定义就能够放在 foo.c 文件中了,这样最终的结果就相似于通过编译器 -O2
级别及其以上的 C++ 模板代码的最终结果。因为 m4 宏语言是图灵完备的,它的功能要比 C 的宏强大了几个数量级。无独有偶,C++ 的模板语言也是图灵完备的。
为了让故事继续下去,咱们不妨去构造一个 foo 模块以供其余模块调用。foo 模块的头文件名曰 foo.h,其内容以下:
#ifndef FOO_H #define FOO_H void * foo(void *x, void *y, void *z); #endif
foo 模块的实现代码即上述的 foo
函数,如今将其保存于一份名曰 foo.c_T 的文件之中:
#include "foo.h" void * foo(void *x, void *y, void *z) { DISTANCE_TYPE d_xz, d_yz; compute_distance_T(x, z, d_xz); compute_distance_T(y, z, d_yz); return less_than_T(d_xz, d_yz) ? x : y; }
而后再制做一个调用 foo 模块的 main.c:
#include <stdio.h> #include <stdlib.h> #include "foo.h" typedef struct { size_t n; double *coord; } Point; int main(void) { double a = {1.0, 2.0, 3.0}; double b = {2.0, 1.0, 3.0}; double c = {4.0, 5.0, 3.0}; Point *x = malloc(sizeof(Point)); Point *y = malloc(sizeof(Point)); Point *z = malloc(sizeof(Point)); x->n = 3; y->n = 3; z->n = 3; x->coord = a; y->coord = b; z->coord = c; if (x == foo(x, y, z)) printf("I am x!\n"); else printf("I am y!\n"); free(z); free(y); free(x); return 0; }
各个模块的代码均已准备就绪,可是尚没法编译,由于 foo.c_T 目前仅仅是一个模板。若基于它产生一份符合 main.c 需求的 foo.c,咱们须要根据 main.c 的须要去定义一组 m4 宏。
而后在 foo.h 与 foo.c_T 的同一目录下建立 foo_env.m4 文件,内容以下:
divert(-1) define(`DISTANCE_TYPE', `double') define(`compute_distance_T', `do { size_t n = ((Point *)($1))->n; assert(n == ((Point *)($2))->n); $3 = 0.0; for (size_t i = 0; i < n; i++) { double d = ((Point *)($2))->coord[i] - ((Point *)($1))->coord[i]; $3 += d * d; } } while (0)') define(`less_than_T', `($1 < $2)') divert(0)dnl
而后,假设 Bash 的工做目录是上述三份文件所在的目录,执行如下命令:
$ echo "include(\`foo_env.m4')dnl" > _t_foo.m4 # 「\'」 是对「`」符号的转义 $ cat foo.c_T >> _t_foo.m4 $ m4 _t_foo.m4 > foo.c $ ls foo.c foo.c_T foo_env.m4 foo.h main.c _t_foo.m4
如今有了一份 foo.c,其中提供了可供其余模块调用的 foo
模板函数的一个实例,即:
void * foo(void *x, void *y, void *z) { double d_xz, d_yz; do { size_t n = ((Point *)(x))->n; assert(n == ((Point *)(z))->n); d_xz = 0.0; for (size_t i = 0; i < n; i++) { double d = ((Point *)(z))->coord[i] - ((Point *)(x))->coord[i]; d_xz += d * d; } } while (0); do { size_t n = ((Point *)(y))->n; assert(n == ((Point *)(z))->n); d_yz = 0.0; for (size_t i = 0; i < n; i++) { double d = ((Point *)(z))->coord[i] - ((Point *)(y))->coord[i]; d_yz += d * d; } } while (0); return (d_xz < d_yz) ? x : y; }
可是,在 main.c 中,咱们仍是没法去调用这个 foo
函数的实例,由于 foo.c 文件中出现了 Point
类型与 C 标准库提供的断言宏 assert
,当 C 编译器进入 foo.c 这个小世界时,它不懂 Point
与 assert
为什么物,因而就开始抱怨。可是此类问题已经属于工程问题了,到目前为止,咱们能够发现,借助 m4 的力量,让 C 代码变成模板代码并不是多么困难的事。
下面开始解决「工程」问题,即如何生成可在 main.c 中使用的 foo
函数模板的实例。首先须要将 Point
数据结构的定义从 main.c 中分离出来,存放于 point.h 文件,并去掉具体的坐标份量类型,而后将 foo.h 中的 foo
函数的声明也移植到 point.h 文件:
#ifndef POINT_H #define POINT_H #include <stdio.h> typedef struct { size_t n; void *coord; } Point; Point * foo(Point *x, Point *y, Point *z); #endif
如今 foo.h 没用了,可将其删除。也就是说, foo
函数应当属于 Point
模块,由于它所涉及的运算的主体是 Point
对象。
一样的道理,应当将 foo.c_T 改名为 point.c_T,而后将其内容修改成:
Point_T_REQUIRES #include "point.h" Point * foo(Point *x, Point *y, Point *z) { DISTANCE_TYPE d_xz, d_yz; compute_distance_T(x, z, d_xz); compute_distance_T(y, z, d_yz); return less_than_T(d_xz, d_yz) ? x : y; }
main.c 的内容变为:
#include <stdio.h> #include <stdlib.h> #include "point.h" int main(void) { double a[] = {1.0, 2.0, 3.0}; double b[] = {2.0, 1.0, 3.0}; double c[] = {4.0, 5.0, 3.0}; Point *x = malloc(sizeof(Point)); Point *y = malloc(sizeof(Point)); Point *z = malloc(sizeof(Point)); x->n = 3; y->n = 3; z->n = 3; x->coord = a; y->coord = b; z->coord = c; if (x == foo(x, y, z)) printf("I am x!\n"); else printf("I am y!\n"); free(z); free(y); free(x); return 0; }
将 foo_env.m4 改名为 point_env.m4,其内容变为:
divert(-1) define(`Point_T_REQUIRES', `#include <assert.h>') define(`DISTANCE_TYPE', `double') define(`compute_distance_T', `do { size_t n = ((Point *)($1))->n; assert(n == ((Point *)($2))->n); $3 = 0.0; for (size_t i = 0; i < n; i++) { double d = ((double *)($2->coord))[i] - ((double *)($1->coord))[i]; $3 += d * d; } } while (0)') define(`less_than_T', `($1 < $2)') divert(0)dnl
而后在 Bash 中执行如下命令:
$ ls main.c point.c_T point_env.m4 point.h $ echo "include(\`point_env.m4')dnl" > _t_point.m4 $ cat point.c_T >> _t_point.m4 $ m4 _t_point.m4 > point.c $ gcc -std=c11 -pedantic -Werror point.c main.c -o test $ ./test I am x!
如今,我但愿在 foo 函数中执行如下运算:
Point * foo(Point *x, Point *y, Point *z) { DISTANCE_TYPE d_xz, d_yz_1th; compute_distance_T(x, z, d_xz); compute_distance_T(y->coord[0], z->coord[0], d_yz_1th); return less_than_T(d_xz, d_yz_1th) ? x : y; }
也就是说,foo
函数如今是先计算 x
到 z
的距离,结果为 d_xz
,而后计算 y
与 z
在第一维度上的距离,结果为 d_yz_1th
,最后根据 d_xz
与 d_yz_1th
的大小返回相应的点对象。
注:上述代码只是表意,实际上它是错误的。由于
Point
的coord
是void *
,它不能参与相似y->coord[0]
这样的下标运算。
不去争论 foo
这样作是否科学,现实中有些问题的确须要作相似的运算,这里仅仅是为了让问题简单一些而作了许多简化。
以前 point_env.m4 中的 compute_distance_T
如今没法知足需求了。由于 compute_distance_T
只考虑了两个 n 维点的运算,未考虑两个点退化为 1 个维度上的坐标份量的状况。若是不想修改 compute_distance_T
的定义,那么就只能将 y->coord[0]
与 z->coord[0]
封装为两个 1 维的 Point
对象,而后再送给 compute_distance_T
,可是这样作,就丢失了用宏抽象抽象这些运算的本意——为了尽可能提升代码的计算性能,结果半途而废。
咱们须要给 compute_distance_T
的一个「特化」的机会。可将 foo 函数的代码修改成:
Point * foo(Point *x, Point *y, Point *z) { DISTANCE_TYPE d_xz, d_yz_ith; compute_distance_T(x, z, d_xz); compute_distance_T(y, z, d_yz_1th, 0); return less_than_T(d_xz, d_yz_1th) ? x : y; }
如今,compute_distance_T
变成了接受 4 个参数的宏,第 4 个参数表示计算两个点在指定维度上的距离。若是不向它提供第 4 个参数,那么它计算的即是两个点的距离。基于这一思路,可将 point_env.m4 中的 compute_distance_T
的定义修改成:
define(`compute_distance_T', `do { ifelse($4, , `dnl size_t n = ((Point *)($1))->n; assert(n == ((Point *)($2))->n); $3 = 0.0; for (size_t i = 0; i < n; i++) { double d = ((double *)($2->coord))[i] - ((double *)($1->coord))[i]; $3 += d * d; }dnl ', `dnl size_t n = ((Point *)($1))->n; assert(n == ((Point *)($2))->n); $3 = ((double *)($2->coord))[$4] - ((double *)($1->coord))[$4]; $3 *= $3; ') } while (0)')
这个新的 compute_distance_T
,可以将:
compute_distance_T(x, z, d_xz); compute_distance_T(y, z, d_yz_1th, 0);
展开为:
do { size_t n = ((Point *)(x))->n; assert(n == ((Point *)(z))->n); d_xz = 0.0; for (size_t i = 0; i < n; i++) { double d = ((double *)(z->coord))[i] - ((double *)(x->coord))[i]; d_xz += d * d; } } while (0); do { size_t n = ((Point *)(y))->n; assert(n == ((Point *)(z))->n); d_yz_1th = ((double *)(z->coord))[0] - ((double *)(y->coord))[0]; d_yz_1th *= d_yz_1th; } while (0);
这正是咱们所指望的。
这个更 foo 的 foo,展现了基于 m4 的 C 模板代码有着很大的弹性,这些要归功于 m4 的变参宏与条件宏。point.c_T 中的代码所具有的这种的弹性,在 C++ 的世界里可经过函数的重载与函数内联来实现。
下面给出一个简单的类型「Trait」示例。
main.c_T:
#include <stdio.h> #include <stdbool.h> int main(void) { char *a = "hello"; float b = 1.0; unsigned int c = 2; if (is_integral_T(a, `char *')) printf("a is integral.\n"); if (!is_integral_T(b, `float')) printf("b is not integral.\n"); if (is_integral_T(c, `unsigned int')) printf("c is integral.\n"); }
traits_env.m4:
注:动用了 m4 的正则表达式。
divert(-1) define(`is_integral_T', `ifelse(regexp(`$2', `^ *char *\* *$'), 0, `true', regexp(`$2', `^ *int *$'), 0, `true', regexp(`$2', `^ *size_t *$'), 0, `true', regexp(`$2', `^ *long *int *$'), 0, `true', regexp(`$2', `^ *char *$'), 0, `true', regexp(`$2', `^ *unsigned +int *$'), 0, `true', `false')') divert(0)dnl
生成模板代码的实例:
$ echo "include(\`traits_env.m4')dnl" > _t_main.m4 $ cat main.c_T >> _t_main.m4 $ m4 _t_main.m4 > main.c
最终所得 main.c,其内容为:
#include <stdio.h> #include <stdbool.h> int main(void) { char *a = "hello"; float b = 1.0; unsigned int c = 2; if (true) printf("a is integral.\n"); if (!false) printf("b is not integral.\n"); if (true) printf("c is integral.\n"); }
值得注意的是,在类型 Trait 这方面,因为 C++ 编译器可以自动推导模板函数的参数类型,因此代码更干净一些。用 m4 宏模拟的「Trait」宏,只能像手动指定了参数类型的 C++ 模板函数那样使用。
m4 宏调用语句与 C 函数的调用语句太过于类似,容易混淆。为了让两者有所区别,而且去掉宏名中的 _T
这个尾巴,咱们可让宏名以一个特殊字符 @
做为前缀。
ifdef(`changeword', `', `errprint(` skipping: no changeword support ')m4exit(`77')')dnl changeword(`[@_a-zA-Z0-9][@_a-zA-Z0-9]*') define(`@compute_distance', `... 宏体 ...')
这样代码模板中的宏调用代码便一目了然:
Point * foo(Point *x, Point *y, Point *z) { DISTANCE_TYPE d_xz, d_yz_ith; @compute_distance(x, z, d_xz); @compute_distance(y, z, d_yz_1th, 0); return less_than_T(d_xz, d_yz_1th) ? x : y; }
changeword
是 m4 的内建宏。在编译安装 m4 时,它是可选的。因此,可能有些 Linux 发行版所提供的 m4 不支持这个宏。稳妥起见,应使用 ifdef
宏进行检测 changeword
是否存在。
本文所用的 m4 宏,只是 m4 宏的用法之冰山一角。即使如此,它在 C 世界这一番拳打脚踢,也足以代表其力量之所在。理论上,既然 m4 宏是一种图灵完备的编程语言,那么 C++ 模板所能作到的事,基于 m4 的 C 代码模板也可以作到。至于如何去作,这要归结为工程问题。至于实际上作此事的复杂程度,如今以为难以分出高下。
身为宏编程语言,m4 有其固有的缺陷。用宏语言编写的复杂程序一旦在运行时出现问题,就很难准肯定位问题所在,由于错误是在宏展开的结果中发现的,发现错误的时候,很难快速肯定它是哪一个宏的展开结果。然而,当 C++ 模板代码出错时,编译器也会给出一堆不知所云的错误信息。即便 C++ 标准如你们所但愿的那样,引入了 Concept,从而能够在模板代码中检测模板参数是否合乎要求,m4 依然有应对之法,由于 m4 能够检测某个宏是否被定义……
也许基于 m4 的 C 代码模板可以体现的一个优点是,咱们不须要去修改语言,也不须要去修改语言的编译器,只须要在语言以外,利用一些经常使用的工具,便可以很大程度上写出兼顾抽象与执行效率的代码。上文除了用了 m4 以外,也用了很基本的 Bash 命令。在实际的工程中,咱们能够继续用 Bash 脚本去实现 C 代码模板实例的生成过程的自动化。
那些未提供泛型或仅提供了「类型擦除」泛型的语言,譬如 Go,Java,理论上也可以基于 m4 实现代码的模板化。
如今,我不打算在实际的项目中去尝试应用基于 m4 的 C 代码模板这种非主流技术。因此上述全部言论,仅为抛砖之见而已。