1. 程式人生 > 其它 >C++ IMPL模式解析(上)

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++ 介面就是採用的這種模式。但後來查資料,發現這種方法有兩個主要的弊端:
虛擬函式開銷:虛擬函式需要使用虛擬函式表指標間接呼叫,執行時才能確定呼叫哪一個函式,無法在編譯期間內聯優化。在上一版中,在編譯期就能確定呼叫哪一個函式,根本用不到虛擬函式的特性。
二進位制相容:虛擬函式是按照索引查詢虛擬函式表來呼叫的,新增或調整虛擬函式順序會造成索引變化,導致新介面在二進位制層面不能相容老介面,就是在末尾增加虛擬函式,也會有風險。