位元詩人 [C 語言] 程式設計教學:使用泛型型別巨集 (_Generic) 撰寫泛型程式

C 語言泛型
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

C11 之前,C 語言缺乏真正的泛型程式支援,雖然我們在先前的文章 (這裡這裡) 中用一些語法特性來模擬泛型,但那些手法皆缺乏型別安全。在 C11 後,透過泛型型別巨集 _Generic 可取得具有型別安全的泛型程式。本文會以一些實例介紹如何使用這項新的語法特性。

具有多型特性的 log 巨集

中實作的 log 公式,根據數字的型別有 log10flog10log10l 等不同函式。這時候,我們可以撰寫以下的巨集來動態調用相對應的函式:

#define log10(x) _Generic((x), \
    log double: log10l, \
    float: log10f, \
    default: log10)(x)

有了這個巨集後,我們只要使用 log10 巨集即可自動調用相對應的函式,不用把型別資訊寫死在程式碼中;藉由這個巨集達到多型的特性。

實作向量相加

接下來,我們來看一個稍長的例子。假定我們現在要實作一個像這樣的數學向量 (vector) 類別:

typedef struct vector_t vector_t;

struct vector_t {
    size_t size;
    double *elements;
};

我們現在以泛型巨集 vector_add 將兩向量相加:

vector_t *u = vector_init(4, 1.0, 2.0, 3.0, 4.0);
vector_t *v = vector_init(4, 2.0, 3.0, 4.0, 5.0);

vector_t *t = vector_add(u, v);

同樣的巨集也可用在向量和純量相加:

vector_t *u = vector_init(4, 1.0, 2.0, 3.0, 4.0);

vector_t *v = vector_add(1.5, u);

C11 之後,這樣的巨集並不難做,同樣用到 _Generic 來宣告該巨集:

#if __STDC_VERSION__ >= 201112L
#define vector_add(u, v) \
    _Generic((u), \
        vector_t *: _Generic((v), \
            vector_t *: vector_add_vv, \
            default: vector_add_vs), \
        default: vector_add_sv)((u), (v))
#else
vector_t * vector_add(vector_t *u, vector_t *v);
#endif  /* C11 */

vector_t * vector_add_vv(vector_t *u, vector_t *v);
vector_t * vector_add_vs(vector_t *v, double s);
vector_t * vector_add_sv(double s, vector_t *v);

在這個巨集中,若 C 編譯器支援泛型巨集敘述,我們就宣告 vector_add 為泛型巨集;反之,則以一般的向量相加為退路 (fallback)。

實際的兩向量相加的實作如下:

#if __STDC_VERSION__ < 201112L
vector_t * vector_add(vector_t *u, vector_t *v)
{
    return vector_add_vv(u, v);
}
#endif  /* C11 */

vector_t * vector_add_vv(vector_t *u, vector_t *v)
{
    assert(vector_size(u) == vector_size(v));

    vector_t *out = vector_new(vector_size(u));
    if (!out) {
        return out;
    }

    for (size_t i = 0; i < vector_size(u); i++) {
        vector_set_at(out, i, vector_at(u, i) + vector_at(v, i));
    }

    return out;
}

由於我們先前有寫退路,也要把這個情形考慮進去。至於向量加法的部分按照數學上的定義去實作即可,不會太困難。

同樣地,可以自己實作向量和純量相加的函式:

vector_t * vector_add_vs(vector_t *v, double s)
{
    vector_t *out = vector_new(vector_size(v));
    if (!out) {
        return out;
    }

    for (size_t i = 0; i < vector_size(v); i++) {
        vector_set_at(out, i, vector_at(v, i) + s);
    }

    return out;
}

vector_t * vector_add_sv(double s, vector_t *v)
{
    return vector_add_vs(v, s);
}

由於加法 (和乘法) 具有交換性,兩個函式可共用同一個實作;但減法 (和除法) 沒有交換性,就要寫兩次。

透過 C11 的這樣特性,我們的向量加法的公開方法更加簡潔。

檢查變數的型別

在 C11 之前,我們無法在 C 語言中對變數進行型別檢查,像是 Python 中的 type 函數基本上是無法取得的功能。不過,在 C11 之後,我們也可以在 C 語言中對變數進行型別檢查了,因為 C11 中的 _Generic 敘述本質上就是一個針對型別特化的 switch 等效敘述,只要用巨集包裝一下,就成了一個型別檢查「函式」。

使用方式如下:

char *str = "Hello World";
assert(type(str) == TYPENAME_POINTER_TO_CHAR);

該巨集定義如下:

enum typename_t {
    TYPENAME_BOOL,
    TYPENAME_CHAR,
    TYPENAME_SIGNED_CHAR,
    TYPENAME_UNSIGNED_CHAR,
    TYPENAME_SHORT,
    TYPENAME_INT,
    TYPENAME_LONG,
    TYPENAME_LONG_LONG,
    TYPENAME_UNSIGNED_SHORT,
    TYPENAME_UNSIGNED_INT,
    TYPENAME_UNSIGNED_LONG,
    TYPENAME_UNSIGNED_LONG_LONG,
    TYPENAME_FLOAT,
    TYPENAME_DOUBLE,
    TYPENAME_LONG_DOUBLE,
    TYPENAME_FLOAT_COMPLEX,
    TYPENAME_DOUBLE_COMPLEX,
    TYPENAME_LONG_DOUBLE_COMPLEX,
    TYPENAME_POINTER_TO_CHAR,
    TYPENAME_POINTER_TO_VOID,
    TYPENAME_OTHER
};

#define type(x) _Generic((x), \
    bool: TYPENAME_BOOL, \
    char: TYPENAME_CHAR, \
    signed char: TYPENAME_SIGNED_CHAR, \
    unsigned char: TYPENAME_UNSIGNED_CHAR, \
    short: TYPENAME_SHORT, \
    int: TYPENAME_INT, \
    long: TYPENAME_LONG, \
    long long: TYPENAME_LONG_LONG, \
    unsigned short: TYPENAME_UNSIGNED_SHORT, \
    unsigned int: TYPENAME_UNSIGNED_INT, \
    unsigned long: TYPENAME_UNSIGNED_LONG, \
    unsigned long long: TYPENAME_UNSIGNED_LONG_LONG, \
    float: TYPENAME_FLOAT, \
    double: TYPENAME_DOUBLE, \
    long double: TYPENAME_LONG_DOUBLE, \
    float complex: TYPENAME_FLOAT_COMPLEX, \
    double complex: TYPENAME_DOUBLE_COMPLEX, \
    long double complex: TYPENAME_LONG_DOUBLE_COMPLEX, \
    char *: TYPENAME_POINTER_TO_CHAR, \
    void *: TYPENAME_POINTER_TO_VOID, \
    default: TYPENAME_OTHER)

由此巨集可看出,其實這個巨集只是跑完一個編譯期的 switch 等效敘述,程式碼並不複雜。

由於這個巨集是後設的,我們無法透過這個巨集涵蓋所有的型別,像是程式設計者自行撰寫的結構型別就無法透過這個巨集偵測出來。不過,重點並不是原封不動地使用這個巨集,而是以這個概念為出發點繼續擴充,就可以將型別檢查套用在自己建立的物件系統上。

結語

由本文的範例可知, C11_Generic 敘述只能算是半套的泛型程式。雖然 _Generic 敘述的確可以自動地根據不同型態來選擇不同的實作,但我們仍然要自行為每個型態實作相同的演算法。所以,_Generic 敘述並沒有真正省到開發者的工作時程,只是外部界面會更加簡潔。

為什麼 C 語言要做出這種特性呢?基本上,這應該是一個具有悠久歷史的程式語言在現代化及相容性間妥協後所產生的結果。至於要不要用,則視自己的需求而定。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。