[C 语言] 程序设计教学:泛型 (Generics),使用泛型类型宏 (`_Generic`)

PUBLISHED ON JAN 21, 2019 — PROGRAMMING

    在 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 等效叙述,程序代码并不复杂。

    由于这个宏是后设的,我们无法透过这个宏涵盖所有的类型,像是程序设计者自行撰写的结构类型就无法透过这个宏侦测出来。不过,重点并不是原封不动地使用这个宏,而是以这个概念为出发点继续扩充,就可以将类型检查套用在自己建立的物件系统上。

    你或许对以下产品有兴趣
    TAGS: C 语言
    All code in the website is licensed under Apache 2.0 unless otherwise mentioned.