舉一個設計不良的例子,假設你要對外公開這個 MyTool 類別,而它的 header 長這樣:
#include <thirdparty/xyz.h> class MyTool { public: void func1(); void func2(const char *str); private: void priv_func1(); thirdparty::xyz mXyz; };
這樣的 header 有兩個缺點,首先是 thirdparty/xyz.h 這個 header 是只有函式庫內部才需要的東西,因為外界能使用的也只有 func1 跟 func2 這兩個成員函數,而它們都不需要 thirdparty::xyz 的詳細定義。如果就這樣讓使用者引用這個 header 的話,他編譯時也需要提供 thirdparty 的 header 路徑,因此相依性被轉嫁了。其次,如果後續 private 的區域有異動,例如說增減了成員函數或資料,會導致這個 header 檔案發生內容變化,致使所有引用這個 header 的程式碼都必需重新編譯。
為了解決此問題,目前主流的對策可分為兩種: Pimpl 與 Pure Virtual Interface.
關於 Pimpl 和 pure virtual interface 的比較可以參考 Niklas Frykholm 這位老兄的一篇精闢介紹. 以下擇其精要:
Pimpl
Pimpl (Private Implementation) 可以套用在任何既有的 class header 上而不需大幅修改. 以上面 MyTool 類別為例,套用 Pimpl 的方式大致是:
- 將既有的 MyTool 類別改名為 MyToolImpl.
- 重新建立一個 MyTool.h 與 MyTool.cpp,其內容如下:
class MyToolImpl; class MyTool { public: MyTool(); virtual ~MyTool(); void func1(); void func2(const char *str); private: MyToolImpl* pImpl; };
// MyTool.cpp #include "MyToolImpl.h" void MyTool::MyTool() { pImpl = new MyToolImpl; } void MyTool::~MyTool() { delete pImpl; } void MyTool::func1() { pImpl->func1(); } void MyTool::func2(const char *str) { pImpl->func2(str); }
MyTool.h 裡面只列出 MyToolImpl 中所有的公開成員函數, 除此之外它只有一個私有資料成員 名為 pImpl, 類型為 MyToolImpl 的指標. 因為只用到指標型態, 所以直接在 class MyTool 的定義前宣告一個 class MyToolImpl; 就好了. 外界不會、也不能引用 MyToolImpl.h, 只能引用 MyTool.h. 如此一來就能將所有內部才需要使用的定義都藏起來. pImpl 會在類別建構時指向一個 new 出來的 MyToolImpl. 而 MyTool 裡面成員函數的實作都只要轉呼叫 pImpl 指向的 MyToolImpl 實例中裡的同名函數就好, 這個轉向的動作又稱為「Forwarding」. Gamedev 上有一個簡而易懂的 Pimpl 範例. 如果是 Windows 上的動態函式庫,透過 __declspec(dllexport) 宣告函式庫界面時, 也只要加在 class MyTool 上即可.
Pure Virtual Interface
Pure Virtual Interface 的運作方式就需要較大規模地修改既有程式碼. 還以 MyTool 當例子, 首先要先定出一個叫 IMyTool 的 class, 裡面一樣是列出所有 MyTool 中所有的公開成員函數, 但不需要私有資料成員. 強制規範所有 IMyTool 中的成員函數都必需為 Pure Virtual Function (前有 virtual 關鍵字, 後接 =0;). 修改原有 class MyTool 的 header, 使其繼承 IMyTool, 如此它自然提供了所有 Pure Virtual Functions 的實作. 另外需要定義一個 factory 函數去產出這個具 IMyTool interface 的物件, 因此要另外寫一個 createMyTool(), 而它裡面就只是 new 出 MyTool 實例然後返回它的 IMyTool 指標. 外界重頭到尾也只知道 IMyTool 而碰觸不到 MyTool 的實作細節. 如果是 Windows 上的動態函式庫,__declspec(dllexport) 這個關鍵字就只需要下在 factory 函數上面, IMyTool 和 MyTool 的類別定義都不需要再加.
// IMyTool.h class IMyTool { public: virtual void func1() = 0; virtual void func2(const char *str) = 0; }; extern IMyTool* createMyTool();
// MyTool.h #include "IMyTool.h" #include <thirdparty/xyz.h> class MyTool : public IMyTool { public: void func1(); void func2(const char *str); private: void priv_func1(); thirdparty::xyz mXyz; };
// MyTool.cpp #include "MyTool.h" // // Implementation of MyTool .... // IMyTool* createMyTool() { return new MyTool(); }
總結 Frykholm 比較兩者後所列的優缺點:
Pimpl: (較多人建議使用)
- 物件產生跟原本一樣可使用 new / delete
- 套上了 Pimpl 之後原 class 的行為還是能被繼承
- 雖然所有 function call 都會被轉向, 但可透過 inline 加速
- 套用在現用的 class 上所花費的代價比 pure virtual interface 低.
Pure Virtual Interface:
- 物件必需透過 factory 函數產生
- 原 class 的行為不能被繼承, 外界只看得到 interface, 成員又都是 pure virtual, 無從繼承. 設計上會受限制.
- 透過 vtable 轉向 function call, 效能不如有做 inline 的 pimpl 模式.
- 套用在現用的 class 上所花費的代價比 pimpl 高.
沒有留言:
張貼留言