基於 m4 的 C 代碼模板化

本文是「在 C 的世界以外」這篇文章的一個大的背景。前端

foo

假設在一個名曰 foo 的函數的內部須要計算點距:正則表達式

void foo(void *x, void *y, ...)
{
        ... 略去若干行 ...
        
        /* 此處是計算點距的代碼,暫時不知如何寫*/
        
        ... 略去若干行 ...
}

foo 所接受的兩個參數 xy 分別是指向兩個點對象的指針。爲了追求通用性,foo 這個函數努力成爲一個不依賴具體數據類型的「泛型」函數。也就是說,如今咱們並不知道 xy 所指向的點對象究竟有着怎樣的數據結構。也許它們的數據結構是:編程

typedef double * Point;

也多是:segmentfault

typedef struct {
        double *coord;
        size_t n;
} Point;

還有多是:數組

typedef struct {
        char **coord;
        size_t n;
        void *attachment;
} Point;

沒錯,點的座標也能夠是字符串數組啊!數據結構

總之,foo 所面對的「點」對象,變化萬千。這種狀況下,如何在 foo 函數中計算 xy 這兩個點的距離?less

C++ 的觀點

若是用 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 的觀點

若是堅持用 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 中完成 xy 的距離的計算,而後將結果經過堆空間傳遞給 foo 函數。

更復雜的 foo

如今,繼續增長 foo 的需求。之因此要計算兩個點之間的距離,確定是用來比較遠近用的。可讓 fooxy 中選出距離點 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_xzd_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_xzd_yz 的四則運算,怎麼計算它們的絕對值,怎麼對它們進行開方……

即便這些都不是問題,最終咱們可以忍受 foo 函數變成了函數指針的亂燉菜,可是它的性能會比 C++ 版本的 foo 函數差許多。由於 C++ 模板代碼會被編譯器編譯爲針對特定數據類型的代碼,它的 d_xzd_yz 位於棧空間,而且還具備內聯函數的優點。C 版本的 foo 函數的境況則很是悲慘,它須要頻繁的調用一組外部函數來完成一些很是簡單的運算,並且 d_xzd_yz 須要堆空間的分配與釋放操做。也許 C 標準庫中的 qsort 就是這樣敗給 C++ STL 中的 sort 函數的。

C 不適合這種細微的抽象,特別是那些可能須要用於 CPU 密集的數值運算的程序的函數不適合這樣細微的抽象。像數組、鏈表、樹、圖這些基本的數據結構,能夠用 void * 表示所存儲的數據,可是這些數據結構自己不須要去作依賴於數據類型的運算,因此能夠對它們進行抽象。若是是寫針對於人類平常活動的一些程序,譬如寫 GUI 庫,寫一些桌面軟件——文本編輯器、閱讀器、文件管理器之類,怎麼抽象都沒大有關係。不過,要寫 GNU Science Library 這樣的庫,就不能去抽象了。GSL 庫幾乎爲每一種基本的數據類型都提供了一套代碼。

C 的用武之地是寫面向特定需求的代碼。若是需求變了,代碼再從新寫一遍……這樣說,彷佛很落伍,可是目前作前端的不是在重寫過去的桌面程序代碼麼?作手機 APP 的,不是在重寫過去的那些桌面程序代碼麼?代碼重寫,增長就業崗位,拯救頹廢的世界經濟……有什麼很差?

m4

單靠 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_TYPEcompute_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 這個小世界時,它不懂 Pointassert 爲什麼物,因而就開始抱怨。可是此類問題已經屬於工程問題了,到目前爲止,咱們能夠發現,藉助 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 更 foo

如今,我但願在 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 函數如今是先計算 xz 的距離,結果爲 d_xz,而後計算 yz 在第一維度上的距離,結果爲 d_yz_1th,最後根據 d_xzd_yz_1th 的大小返回相應的點對象。

注:上述代碼只是表意,實際上它是錯誤的。由於 Pointcoordvoid *,它不能參與相似 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

下面給出一個簡單的類型「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 代碼模板這種非主流技術。因此上述全部言論,僅爲拋磚之見而已。

相關文章
相關標籤/搜索