本文是「在 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 代碼模板這種非主流技術。因此上述全部言論,僅爲拋磚之見而已。