前言
在 C11 之前,C 語言缺乏真正的泛型程式支援,雖然我們在先前的文章 (這裡和這裡) 中用一些語法特性來模擬泛型,但那些手法皆缺乏型別安全。在 C11 後,透過泛型型別巨集 _Generic
可取得具有型別安全的泛型程式。本文會以一些實例介紹如何使用這項新的語法特性。
具有多型特性的 log 巨集
在 log10f
、log10
、log10l
等不同函式。這時候,我們可以撰寫以下的巨集來動態調用相對應的函式:
#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 語言要做出這種特性呢?基本上,這應該是一個具有悠久歷史的程式語言在現代化及相容性間妥協後所產生的結果。至於要不要用,則視自己的需求而定。