C++ IMPL模式解析(上)
https://blog.csdn.net/myw31415926/article/details/127722899
拋磚引玉
試想一個問題,如果有一套收發資料的網路介面,需要提供給其他同事或廠家使用,包含標頭檔案和動態庫,假設標頭檔案如下:
// 版本1
class NetworkV1 {
public:
int Send(const std::string str);
int Recv(std::string &str);
private:
int sockfd;
char buf[1024];
};
使用者直接 include 標頭檔案,連結庫檔案即可。方法上沒有問題,但問題是標頭檔案中暴露的資訊太多了,比如 private 成員變數,而且如果以後的版本中需要增加或刪除某些變數,還需要通知使用者修改標頭檔案,太麻煩了。
為了解決這個問題,實現介面與實現分離,所以引入了 IMPL 模式。
C++ IMPL 模式
這裡的 IMPL 其實就是 implement,即實現的意思,個人覺得 IMPL 嚴格上來講,並不算一個設計模式,只是一個更好的 隱藏實現的方法 。已 C++ 為例,它不僅僅是將類的宣告和實現放在不同的檔案中,更重要的是隱藏細節,只暴露使用者必須的介面部分。先看一版改進的程式碼:
// network.h // 版本2 class NetworkV2 { public: int Send(const std::string str); int Recv(std::string &str); private: struct Impl; std::shared_ptr<Impl> impl; }; // network.cpp // 版本2 struct NetworkV2::Impl { int sockfd; char buf[1024]; }; int NetworkV2::Send(const std::string str) { // TODO ... // send(impl->sockfd, str.c_str(), str.size(), 0); return str.size(); } int NetworkV2::Recv(std::string &str) { // TODO ... // recv(impl->sockfd, impl->buf, 1024, 0); return str.size(); }
這樣就做到了隱藏類中的成員變量了,核心思想就是 將成員變數打包放在一個結構體中 ,無論以後的版本中有無刪減成員變數,都不會對標頭檔案造成任何影響,這是目前 C++ IMPL 中非常常見的一種呼叫方法。類似於 C 語言中的 void* 指標,可以在需要的時候轉換成任意物件。
完全隱藏成員變數
但是上一種模式還是會有 private 的成員變數,如果是想要完全隱藏,只保留介面呢?我在學習上交所 CTP 介面的時候,還看見過一種新的方法,核心思想是 使用虛擬函式和繼承 。這種模式標頭檔案中只保留介面,不會有任何的成員變數,程式碼如下:
// network.h // 版本3 class NetworkV3 { public: virtual int Send(const std::string str) = 0; virtual int Recv(std::string &str) = 0; // 建立和銷燬函式 static NetworkV3* New(); static void Delete(NetworkV3 *net); }; // network.cpp // 版本3 class NetworkV3Impl final : public NetworkV3 { public: int Send(const std::string str) override { std::cout << "NetworkV3Impl::Send: " << str << std::endl; return str.size(); } int Recv(std::string &str) { str = "ok"; std::cout << "NetworkV3Impl::Recv: " << str << std::endl; return str.size(); } }; // 建立和銷燬函式 NetworkV3* NetworkV3::New() { return (new NetworkV3Impl()); } void NetworkV3::Delete(NetworkV3 *net) { delete (NetworkV3Impl*)net; }
雖然消除了 private 成員變數,但增加了兩個靜態成員函式 New 和 Delete ,用於建立和銷燬物件,也不需要使用者自己管理記憶體,使用上很方便,像 CTP 的 C++ 介面就是採用的這種模式。但後來查資料,發現這種方法有兩個主要的弊端:
虛擬函式開銷:虛擬函式需要使用虛擬函式表指標間接呼叫,執行時才能確定呼叫哪一個函式,無法在編譯期間內聯優化。在上一版中,在編譯期就能確定呼叫哪一個函式,根本用不到虛擬函式的特性。
二進位制相容:虛擬函式是按照索引查詢虛擬函式表來呼叫的,新增或調整虛擬函式順序會造成索引變化,導致新介面在二進位制層面不能相容老介面,就是在末尾增加虛擬函式,也會有風險。