home · archive · links · projects

在C語言中模擬RAII

資源安全在C中要比C++中困難不少。C++中有了RAII機制,資源安全可謂得心應手。無怪乎,Stroustrup的《C++程序設計語言》的前半本都在寫內存安全和資源安全,而這些也全是用RAII保證的。局部變量一旦出了作用域,其析構函數就被調用了,非常方便。

然而,C語言中就沒有這種好用的工具,比如說:

int foo() {
    FILE* fp = fopen("bar", "w");
    if (f == 0) {
        error("failed to open file");
        return -1;
    }
    int ret = do_something(fp);
    if (ret < 0) {
        error("failed to process file");
        fclose(fp);
        return -1;
    }
    fprintf(fp, "this end it");
    fclose(fp);
    return 0;
}

這裏僅僅是一個簡單的例子,fclose在這裏程序出現了兩次;然而,當程序的控制流繁雜起來的時候,資源回收就變得駭人起來。不似C++中,只需要打開一個ofstream,然後把剩下的交給析構函數就好。

除了C++以外,別的語言中也有類似的機制,比如說Javas和Go裏面都有垃圾回收,用來處理內存。至於別的資源,比如文件柄、網絡連接、互斥鎖等等,在Java裏面會用try…catch…finally…處理,而Go語言裏面會用defer來處理。

所以C裏面應該怎麼辦呢?所幸,gcc提供了一個cleanup擴展,可以用來註冊析構函數。上面那個關閉文件的例子就可以用這個擴展重寫成下面這樣:

void close_file(FILE** fp_ptr) {
    if (*fp_ptr == NULL) return;
    fprintf(*fp_ptr, "file is closed\n");
    fclose(*fp_ptr);
}

int foo() {
    __attribute__((cleanup(close_file))) FILE* fp = fopen("bar", "w");
    if (fp == NULL) {
        error("failed to open file");
        return -1;
    }    
    int ret = do_something(fp);
    if (ret < 0) {
        error("failed to process file");
        return -1;
    }
    fprintf(fp, "this end it\n");
    return 0;
}

有了這個cleanup attribute, close_file就可以自動執行了,省去了手動管理的困擾。

爲了讓代碼更緊湊,還可以加一個詞法宏。

#define CLEANUP(func) __attribute__((cleanup(func)))

互斥鎖也類似:

pthread_mutex_t mutex;
int count;

void unlock_mutex(pthread_mutex_t **mutex_ptr) {
    pthread_mutex_unlock(*mutex_ptr);
}

void *thread_run(void *arg){
    int i;
    int ret = pthread_mutex_lock(&mutex);
    if (ret != 0) {
        error("failed to acqure lock");
        return 0;
    }
    CLEANUP(unlock_mutex) pthread_mutex_t *defer_mutex = &mutex;
    for (i = 0; i < 3; i++) {
        printf("[%ld]count: %d\n", pthread_self(), ++count);
    }
    return 0;
}

int main() {
    pthread_t threads[10];
    for (int i = 0; i < 10; i++) {
        int res = pthread_create(&threads[i], NULL, thread_run, NULL);
        if (res) error("create thread error");
    }
    for (int i = 0; i < 10; i++) {
        void *ret;
        pthread_join(threads[i], &ret);
    }
    return 0;
}

雖說這是個gcc擴展,不過Clang/LLVM工具鏈也是支持的。

如果想要更通用的寫法,還可以用goto語句實現。雖然goto語句一般被認爲一種不好的實踐,但是在資源回收這個場景中,其實反而被認爲是一種好的做法:

int foo() {
    FILE* fp = fopen("bar", "w");
    if (f == 0) {
        error("failed to open file");
        goto clean_0;
    }
    int ret = do_something(fp);
    if (ret < 0) {
        error("failed to process file");
        goto clean_1;
    }
    fprintf(fp, "this end it");
    fclose(fp);
    return 0;

clean_1:
    fclose(fp);
clean_0:
    return -1;
}

或者,也可以嘗試用宏:

int foo() {
    FILE* fp = NULL;
    #define DEFER \
        if (fp != NULL) fclose(fp);

    fp = fopen("bar", "w");
    if (f == 0) {
        error("failed to open file");
        DEFER return -1;
    }
    int ret = do_something(fp);
    if (ret < 0) {
        error("failed to process file");
        DEFER return -1;
    }
    fprintf(fp, "this end it");
    DEFER return 0;
    #undef DEFER
}

綜合起來,感覺還是用goto語句最佳。

另一邊,還有一個給C語言加defer的提案,不過能不能進標準誰也不知道,就拭目以待吧。


© Licensed under CC BY-NC-SA 4.0 if not specified otherwise.
Email: dzshy [at] outlook [dot] com