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

【分享本文】
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 Vector;
    
    struct vector {
        size_t size;
        double *elements;
    };

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

    Vector *u = vector_init(4, 1.0, 2.0, 3.0, 4.0);
    Vector *v = vector_init(4, 2.0, 3.0, 4.0, 5.0);
    
    Vector *t = vector_add(u, v);

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

    Vector *u = vector_init(4, 1.0, 2.0, 3.0, 4.0);
    
    Vector *v = vector_add(1.5, u);

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

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

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

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

    #if __STDC_VERSION__ < 201112L
    Vector * vector_add(Vector *u, Vector *v)
    {
        return vector_add_vv(u, v);
    }
    #endif
    
    Vector * vector_add_vv(Vector *u, Vector *v)
    {
        assert(vector_size(u) == vector_size(v));
    
        Vector *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 * vector_add_vs(Vector *v, double s)
    {
        Vector *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 * vector_add_sv(double s, Vector *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 等效敘述,程式碼並不複雜。

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

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk
    標籤: C 語言