C++14 都出來了,C++11 仍然有很多東西我沒有好好吸收,主要是因為在現實中沒有什麼使用機會,只是在閒暇時囫圇吞棗般看了一堆文章而已。舉例來說,inline namespace 就是一個我最近才仔細研究使用情境的機制。

C++11 的 inline namespace 主要功能在於,讓撰寫者可以在 namespace 建立抽象層,在底層切換不同版本。

理想化的使用情境是這樣的,假如有一個程式庫叫做 mylib,最初的實作如下:

namespace mylib {
    inline namespace v1 {
        void foo();
    }
}

當一個外部使用者在程式碼撰寫:

mylib::foo();

實際上使用到的函式為 mylib::v1::foo()。

隨著時間演進,這個程式庫推出了新版本。這個時候不必廢棄舊的程式碼,而是把新程式碼放到新的 inline namespace。

namespace mylib {
    namespace v1 {
        void foo();
    }

    inline namespace v2 {
        void foo();
    }
}

外部程式碼使用 mylib::foo() 時,會對應到 mylib::v2::foo()。

雖然原始碼中 mylib::foo() 對應的函式改變了,但舊版的 mylib::v1::foo() 實作仍會繼續被編入二進碼中,而且保持相同的 ABI,依賴舊版二進制檔的程式可以毫無困難的從新版二進制檔中取得所需。

當然,如果由原始碼從頭編譯,那麼外部程式看到的會是 v2 的內容,要是 v2 打破了 v1 的介面,依賴 v1 介面的原始碼就會編譯失敗。這一點並不難解決,只要定義一些 preprocessor 切換 inline namespace 即可。

這裡要特別強調的是,inline namespace 只不過是在原始碼層級將名稱引入到外層 namespace,這些名稱並不會真正被定義在外面。像下面這樣的例子並不會造成多重定義問題,編譯器會將三個 foo() 視為定義在不同地方的三個獨立實體:

namespace mylib {
    inline namespace v1 {
        void foo();
    }

    inline namespace v2 {
        void foo();
    }

    void foo();
}

只不過像下面這樣的呼叫會是 ambiguous call:

mylib::foo();   // ambiguous

因為有三個函式皆符合條件且順位相同,必須要提供足夠的資訊才有辦法區別:

mylib::v1::foo();
mylib::v2::foo();

至於真正的 mylib::foo() 呢?就我的理解是恐怕沒有辦法使用了。

乍看之下 inline namespace 就只是暴露內部的名稱而已,這不是 unsing namespace 一直都可以做到的事嗎?

namespace mylib {
    namespace v1 {
        struct Dummy {};
        void foo();
    }

    using namespace v1;
}
...

mylib::Dummy d;
mylib::foo();

雖然在多數情境下 using namespace 幾乎可以達成目標,但遇到 ADL 就會露出破綻。因為 using 只能導致「被看到」,然而 ADL 在乎的卻是「被定義」。

有些泛型程式庫設計相當依賴 ADL 機制,STL 有不少地方運用了不指定 namespace 的 operator<() 或 swap(),讓 ADL 有作用的空間。

舉例來說,假設原本的程式庫沒有為 Dummy 提供 operator<(),有位不知 mylib 內部設計的使用者想用 std::sort 排序 Dummy:

mylib::Dummy dummy[10];
...
std::sort(dummy, dummy+10);

為了讓編譯器找到外部使用者提供的 operator<(),常見做法是將 operator<() 放到定義 Dummy 的同一個 namespace 之下,於是編譯器就可以透過 ADL 找到。

namespace mylib {
    bool operator<(const Dummy& lhs, const Dummy& rhs);
}

不過在上面的例子裡,這個做法將會失敗,應該要將 operator<() 放在 mylib::v1 之下才對。外部使用者必須了解這個把戲才有辦法把事情做對,可是這正是原作者千方百計想隱瞞的實作細節。

除了 ADL 外,還有一種情形是 namespace 需要提供模板給外部特化時,用 using namespace 虛擬也會造成失敗。

如果 v1 是 inline namespace,上面的用法就完全不成問題。這就是 inline namespace 比 using 多提供的能力,inline namespace 不只是暴露名稱,而且在在定義範圍查詢時會被視為等效。

另一個 inline namespace 和 using 的重大差異在於名稱覆蓋的行為:

namespace mylib {
    namespace v1 {
        void bar(const char* cstr);
        void bar(int n);
    }
    using namespace v1;
    void bar();
}
...

mylib::bar();       // ok
mylib::bar(101);    // error
mylib::bar("bye");  // error

雖然 using 讓 v1 內兩個帶參數的 bar() 暴露在 mylib 下,但外層的 mylib::bar() 又把兩者的名稱遮蓋掉了。反過來說,inline namespace 就不會有這樣的問題:

namespace mylib {
    inline namespace v1 {
        void bar(const char* cstr) {};
        void bar(int n) {};
    }
    void bar() {};
}
...

mylib::bar();       // ok
mylib::bar(101);    // ok
mylib::bar("bye");  // ok

雖然 inline namespace 解決了一部份問題,想在 namespace 上建立抽象層,仍然有很多事情得傷腦筋。

主要問題在於,對於同一個功能模組的不同版本,可以預期大部分程式碼仍是共通的。如果直接安排成多份獨立實作,將導致不同 namespace 之間存在大量重複程式碼,對維護來說是一件非常糟糕的事。另一方面,也可以把共通的部份抽取出來,只在介面層虛擬出不同的 namespace。不過這有可能使程式碼階層關係變得錯綜複雜,程式的邏輯架構或者檔案樹架構皆會受影響。

關於這點,在《The C++ Programming Language》第四版裡,Stroustrup 提出了一個運用 #include 技巧的手法解決重複程式碼的問題。這類的做法大概不會合乎某些人的美學,我也還沒仔細思考這個運用在現實中會遇到哪些問題。

在我寫文章的此時,網路關於 inline namespace 的討論多停留在語法介紹的層次上,至於實用上的 best practice、DOs and DON'Ts 似乎仍然不明朗。

在目前的 Google C++ Style Guide 裡面仍然建議不使用 inline namespace。但我已經在一些 boost 底下的程式庫發現 inline namespace 的蹤跡了。

novus 發表在 痞客邦 PIXNET 留言(0) 人氣()