[C 語言] 程式設計教學:使用前置處理器 (Preprocessor) 撰寫擬泛型程式

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
【贊助商連結】

    在前文中,我們展示了使用指向 void 的指標來實作的泛型程式。在本文中,我們會展示以 C 前置處理器 (C 巨集) 來實作的泛型程式。這個方法是非主流的手法,因為 (1) 沒有型別安全,(2) 難以除錯。這己經算是一種反模式 (anti-pattern),我們仍然展示這個方法,讀者可自行決定要不要使用在自己的專案中。

    我們將完整的程式碼放在這裡,有興趣的讀者可自行追蹤,本文僅節錄部分內容。

    我們先從外部程式來看如何使用此泛型佇列:

    #include <assert.h>
    #include <stdbool.h>
    #include <stdio.h>
    #include "queue.h"
    
    // Declare queue function once per type.
    queue_declare(int);
    
    int main(void)
    {
        bool failed = false;
    
        // Queue: NULL
        queue_class(int) *q = queue_new(int,
            (queue_params(int)) { .item_free = NULL });
        if (!q) {
            perror("Failed to allocate queue q");
            return false;
        }
    
        // Queue: 9 -> NULL
        if (!queue_enqueue(int, q, 9)) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        // Queue: 9 -> 5 -> NULL
        if (!queue_enqueue(int, q, 5)) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        // Queue: 9 -> 5 -> 7 -> NULL
        if (!queue_enqueue(int, q, 7)) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        // Queue: 5 -> 7 -> NULL
        if (queue_dequeue(int, q) != 9) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        // Queue: 7 -> NULL
        if (queue_dequeue(int, q) != 5) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        // Queue: NULL
        if (queue_dequeue(int, q) != 7) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        if (!queue_is_empty(int, q)) {
            failed = true;
            goto QUEUE_FREE;
        }
    
    QUEUE_FREE:
        queue_free(int, q);
    
        if (failed) {
            return 1;
        }
    
        return 0;
    }

    由此可見,我們不需要額外的 box class 就可以在基礎型別中使用此泛型程式。附帶一提,由於我們的「函式」是從巨集擴張而來的,直接使用指標會引發錯誤,要額外加入以下型別定義:

    typedef struct klass * klass_p;

    之後再將 klass_p 做為型別宣告,塞入我們的巨集即可。

    我們來看 queue_declare 的實際內容:

    #define queue_declare(type) \
        queue_node_declare(type) \
        queue_class_declare(type) \
        queue_node_new_declare(type) \
        queue_params_declare(type) \
        queue_class_new_declare(type) \
        queue_class_free_declare(type) \
        queue_class_is_empty_declare(type) \
        queue_class_peek_declare(type) \
        queue_class_enqueue_declare(type) \
        queue_class_dequeue_declare(type)

    由此可知,其實 queue_declare 只是一個宣告其他巨集的巨集。

    以下是一些「函式」的宣告:

    #define queue_new(type, item_free) \
        queue_##type##_new(item_free)
    
    #define queue_free(type, self) \
        queue_##type##_free(self)
    
    #define queue_is_empty(type, self) \
        queue_##type##_is_empty(self)
    
    #define queue_peek(type, self) \
        queue_##type##_peek(self)
    
    #define queue_enqueue(type, self, data) \
        queue_##type##_enqueue(self, data)
    
    #define queue_dequeue(type, self) \
        queue_##type##_dequeue(self)

    在泛型程式中,我們不能將型別預先寫死,所以要由巨集使用者提供型別資訊,在巨集中會擴張成相對應的函式。

    以下是巨集版本的類別宣告:

    #define queue_class(type) queue_##type
    
    #define queue_class_declare(type) \
        typedef struct queue_##type##_s queue_class(type); \
        struct queue_##type##_s { \
            freeFn item_free; \
            queue_node(type) *head; \
            queue_node(type) *tail; \
        };

    仔細觀察,可發現本質上仍是佇列類別,只是把型別的地方在編譯時代換掉。

    以下是巨集版本的建構函式:

    #define queue_class_new_declare(type) \
        queue_class(type) * queue_##type##_new(queue_params(type) params) \
        { \
            queue_class(type) * q = malloc(sizeof(queue_class(type))); \
            if (!q) { \
                return q; \
            } \
            q->item_free = params.item_free; \
            q->head = NULL; \
            q->tail = NULL; \
            return q; \
        }

    由於是巨集的緣故,程式碼會比一般的建構函式難閱讀一些。

    以下是巨集版本的解構函式:

    #define queue_class_free_declare(type) \
        void queue_##type##_free(void *self) \
        { \
            if (!self) { \
                return; \
            } \
            queue_node(type) *curr = ((queue_class(type) *) self)->head; \
            freeFn fn = ((queue_class(type) *) self)->item_free; \
            queue_node(type) *temp; \
            while (curr) { \
                temp = curr; \
                curr = curr->next; \
                if (fn) { \
                    fn(temp->data); \
                } \
                free(temp); \
            } \
            free(self); \
        }

    在節點內的數據是基礎型別時,不需要手動釋放記憶體,但該數據是指標型別時則要。所以本程式檢查 fn 是否存在,當 fn 存在時呼叫該函式把記憶體放掉。

    以下是從將資料推入佇列前端的巨集:

    #define queue_class_enqueue_declare(type) \
        bool queue_##type##_enqueue(queue_class(type) *self, type data) \
        { \
            assert(self); \
            queue_node(type) *node = queue_node_new(type, data); \
            if (!node) { \
                return false; \
            } \
            if (!(self->tail)) { \
                self->head = node; \
                self->tail = node; \
                return true; \
            } \
            self->tail->next = node; \
            node->prev = self->tail; \
            self->tail = node; \
            return true; \
        }

    其實和一般的佇列函式相差不大。

    以下是將資料從佇列前端移出的巨集:

    #define queue_class_dequeue_declare(type) \
        type queue_##type##_dequeue(queue_class(type) *self) \
        { \
            assert(self); \
            if (self->head == self->tail) { \
                type popped = self->head->data; \
                free(self->head); \
                self->head = NULL; \
                self->tail = NULL; \
                return popped; \
            } \
            queue_node(type) *curr = self->head; \
            type popped = curr->data; \
            self->head = curr->next; \
            free(curr); \
            return popped; \
        }

    在此處,我們沒有拷貝節點內的數據,如果碰到指標型別時,巨集使用者要自行負責釋放該數據。

    稍微想一下前置處理器的工作方式,就會知道這樣的「函式庫」實用性偏低。在巨集擴張後,原本的程式已經被代換掉了,程式的行數可能和原本的程式碼有相當的差距,我們無法直接從編譯器的錯誤訊息得知到底是在原程式的那一行出錯;此外,如果實際寫過一些巨集程式就知道 C 巨集容易出錯且除錯相對困難;這樣的程式也缺乏靜態型別語言應有的型別安全。雖然我們可以用 C 前置處理器寫泛型程式,但應謹慎為之。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【支持站長】
    Buy me a coffeeBuy me a coffee
    【贊助商連結】
    標籤: C 語言
    【分類瀏覽】