你知道的C++接口方法有這些嗎?

程序開發的時候常常會使用到接口。衆所周知,C++語言層面並無接口的概念,但並不意味着C++不能實現接口的功能。相反,正是因爲C++語言沒有提供標準的接口,致使實際實現接口的方法多種多樣。那麼C++有哪些實現接口的方法呢,不一樣的方法又適用於哪些場景呢?本文分享在C++接口工程實踐上的一些探索心得。編程


 

1、 接口的分類異步

接口按照功能劃分能夠分爲調用接口與回調接口:編程語言

調用接口ide

一段代碼、一個模塊、一個程序庫、一個服務等(後面都稱爲系統),對外提供什麼功能,以接口的形式暴露出來,用戶只須要關心接口怎麼調用,不用關心具體的實現,便可使用這些功能。這類被用戶調用的接口,稱爲調用接口。函數

調用接口的主要做用是解耦,對用戶隱藏實現,用戶只須要關心接口的形式,不用關心具體的實現,只要保持接口的兼容性,實現上的修改或者升級對用戶無感知。解耦以後也方便多人合做開發,設計好接口以後,各模塊只經過接口進行交互,各自完成各自的模塊便可。性能

回調接口學習

系統定義接口,由用戶實現,註冊到系統中,系統有異步事件須要通知用戶時,回調用戶註冊的接口實現。系統定義接口的形式,但無需關心接口的實現,而是接受用戶的註冊,並在適當的時機調用。這類由系統定義,用戶實現,被系統調用的接口,稱爲回調接口。測試

回調接口的主要做用是異步通知,系統定義好通知的接口,並在適當的時機發出通知,用戶接收通知,並執行相應的動做,用戶動做執行完後控制權交還給系統,用戶動做能夠給系統返回一些數據,以決定系統後續的行爲。優化

二 、調用接口ui

咱們以一個Network接口爲例,說明C++中的調用接口的定義及實現,示例以下:

class Network

{

public:

    bool send(const char* host,

              uint16_t port,

              const std::string& message);

}

Network接口如今只須要一個send接口,能夠向指定地址發送消息。下面咱們用不一樣的方法來定義Network接口。

虛函數

虛函數是定義C++接口最直接的方式,使用虛函數定義Network接口類以下:

class Network

{

public:

    virtual bool send(const char* host,

                      uint16_t port,

                      const std::string& message) = 0;

    static Network* New();

    static void Delete(Network* network);

}

將send定義爲純虛函數,讓子類去實現,子類不對外暴露,提供靜態方法New來建立子類對象,並以父類Network的指針形式返回。接口的設計通常遵循對象在哪建立就在哪銷燬的原則,所以提供靜態的Delete方法來銷燬對象。由於對象的銷燬封裝在接口內部,所以Network接口類能夠不用虛析構函數。

使用虛函數定義接口簡單直接,可是有不少弊端:

虛函數開銷:虛函數調用須要使用虛函數表指針間接調用,運行時才能決定調用哪一個函數,沒法在編譯連接期間內聯優化。實際上調用接口在編譯期間就能肯定調用哪一個函數,無需虛函數的動態特性。

二進制兼容:因爲虛函數是按照索引查詢虛函數表來調用,增長虛函數會形成索引變化,新接口不能在二進制層面兼容老接口,並且因爲用戶可能繼承了Network接口類,在末尾增長虛函數也有風險,所以虛函數接口一經發布,難以修改。

指向實現的指針

指向實現的指針是C++比較推薦的定義接口的方式,使用指向實現的指針定義Network接口類以下:

class NetworkImpl;

class Network

{

public:

    bool send(const char* host,

              uint16_t port,

              const std::string& message);

    Network();

    ~Network();

private:

    NetworkImpl* impl;

}

Network的實現經過impl指針轉發給NetworkImpl,NetworkImpl使用前置聲明,實現對用戶隱藏。使用指向實現的指針的方式定義接口,接口類對象的建立和銷燬能夠由用戶負責,所以用戶能夠選擇將Network類的對象建立在棧上,生命週期自動管理。

使用指向實現的指針定義接口具備良好的通用性,用戶可以直接建立和銷燬接口對象,而且增長新的接口函數不影響二進制兼容性,便於系統的演進。

指向實現的指針增長了一層調用,儘管對性能的影響幾乎能夠忽略不計,但不太符合C++的零開銷原則,那麼問題來了,C++可否實現零開銷的接口呢?固然能夠,即下面要介紹的隱藏的子類。


 

隱藏的子類

隱藏的子類能夠實現零開銷的接口,思想很是簡單。調用接口要實現的目標是解耦,主要就是隱藏實現,也即隱藏接口類的成員變量,若是能將接口類的成員變量都移到另外一個隱藏的實現類中,接口類就不須要任何成員變量,也就實現了隱藏實現的目的。隱藏的子類就是這個隱藏的實現類,使用隱藏的子類定義Network接口類以下:

class Network

{

public:

    bool send(const char* host,

              uint16_t port,

              const std::string& message);

    static Network* New();

    static void Delete(Network* network);

protected:

    Network();

    ~Network();

}

Network接口類只有成員函數(非虛函數),沒有成員變量,而且構造函數和析構函數都申明爲protected。提供靜態方法New建立對象,靜態方法Delete銷燬對象。New方法的實現中建立隱藏的子類NetworkImpl的對象,並以父類Network指針的形式返回。NetworkImpl類中存放Network類的成員變量,並將Network類聲明爲friend:

class NetworkImpl : public Network

{

    friend class Network;

private:

    //Network類的成員變量

}

Network的實現中,建立隱藏的子類NetworkImpl的對象,並以父類Network指針的形式返回,經過將this強制轉換爲NetworkImpl的指針,訪問成員變量:

bool Network::send(const char* host,

                  uint16_t port,

                  const std::string& message)

{

    NetworkImpl* impl = (NetworkImpl*)this;

    //經過impl訪問成員變量,實現Network

}

static Network* New()

{

    return new NetworkImpl();

}

static void Delete(Network* network)

{

    delete (NetworkImpl*)network;

}

使用隱藏的子類定義接口一樣具備良好的通用性和二進制兼容性,同時沒有增長任何開銷,符合C++的零開銷原則。


 

三 、回調接口

一樣以Network接口爲例,說明C++中的回調接口的定義及實現,示例以下:

class Network

{

public:

    class Listener

    {

    public:

        void onReceive(const std::string& message);

    }

    bool send(const char* host,

              uint16_t port,

              const std::string& message);

    void registerListener(Listener* listener);

}

如今Network須要增長接收消息的功能,增長Listener接口類,由用戶實現,並註冊其對象到Network中後,當有消息到達時,回調Listener的onReceive方法。

虛函數

使用虛函數定義Network接口類以下:

class Network

{

public:

    class Listener

    {

    public:

        virtual void onReceive(const std::string& message) = 0;

    }

    bool send(const char* host,

              uint16_t port,

              const std::string& message);

    void registerListener(Listener* listener);

}

將onReceive定義爲純虛函數,由用戶繼承實現,因爲多態的存在,回調的是實現類的方法。

使用虛函數定義回調接口簡單直接,但一樣存在和調用接口中使用虛函數一樣的弊端:虛函數調用開銷,二進制兼容性差。

函數指針

函數指針是C語言的方式,使用函數指針定義Network接口類以下:

class Network

{

public:

    typedef void (*OnReceive)(const std::string& message, void* arg);

    bool send(const char* host,

              uint16_t port,

              const std::string& message);

    void registerListener(OnReceive listener, void* arg);

}

使用函數指針定義C++回調接口簡單高效,但只適用於回調接口中只有一個回調函數的情形,若是Listener接口類中要增長onConnect,onDisconnect等回調方法,單個函數指針沒法實現。另外函數指針不太符合面向對象的思想,能夠換成下面要介紹的std::function。

std::function

std::function提供對可調用對象的抽象,可封裝簽名相符的任意的可調用對象。使用std::function定義Network接口類以下:

class Network

{

public:

    typedef std::function<void(const std::string& message)> OnReceive;

    bool send(const char* host,

              uint16_t port,

              const std::string& message);

    void registerListener(const OnReceive& listener);

}

std::function能夠很好的取代函數指針,配合std::bind,具備很好的通用性,於是被廣受推崇。但std::function一樣只適用於回調接口中只有一個回調方法的情形。另外,std::function比較重量級,使用上面的便利卻會帶來了性能上的損失,有人作過性能對比測試,std::function大概比普通函數慢6倍以上,比虛函數還慢。

類成員函數指針

類成員函數指針的使用比較靈活,使用類成員函數指針定義Network接口類以下:

class Network

{

public:

    class Listener

    {

    public:

        void onReceive(const std::string& message);

    }

    typedef void (Listener::* OnReceive)(const std::string& message);

    bool send(const char* host,

              uint16_t port,

              const std::string& message);

    void registerListener(Listener* listener, OnReceive method);

    template<typename Class>

    void registerListener(Class* listener,

        void (Class::* method)(const std::string& message)

    {

        registerListener((Listener*)listener, (OnReceive)method);

    }

}

由於類成員函數指針必須和類對象一塊兒使用,因此Network的註冊接口須要同時提供對象指針和成員函數指針,registerListener模板函數可註冊任意類的對象和相應符合簽名的方法,無需繼承Listener,與接口類解耦。

使用類成員函數指針定義C++回調接口靈活高效,可實現與接口類解耦,而且不破壞面向對象特性,可很好的取代傳統的函數指針的方式。

類成員函數指針一樣只適用於回調接口中只有一個回調方法的情形,若是有多個回調方法,須要針對每個回調方法提供一個類成員函數指針。那麼有沒有方法既能實現與接口類解耦,又能適用於多個回調方法的場景呢?參考下面介紹的非侵入式接口。


 

四 、非侵入式接口

Rust中的Trait功能很是強大,能夠在類外面,不修改類代碼,實現一個Trait,那麼C++可否實現Rust的Trait的功能呢?仍是以Network接口爲例,假設如今Network發送須要考慮序列化,從新設計Network接口,示例以下:

定義Serializable接口:

class Serializable

{

public:

    virtual void serialize(std::string& buffer) const = 0;

};

Network接口示例:

class Network

{

public:

    bool send(const char* host,

              uint16_t port,

              const Serializable& s);

}

Serializable接口至關於Rust中的Trait,如今一切實現了Serializable接口的類的對象都可以經過Network接口發送。那麼問題來了,可否在不修改類的定義的同時,實現Serializable接口呢?假如咱們要經過Network發送int類型的數據,可否作到呢?答案是確定的:

1. class IntSerializable : public Serializable

{

public:

    IntSerializable(const int* i) :

        intThis(i)

    {

    }

    IntSerializable(const int& i) :

        intThis(&i)

    {

    }

    virtual void serialize(std::string& buffer) const override

    {

        buffer += std::to_string(*intThis);

    }

private:

    const int* const intThis;

};

有了實現了Serializable接口的IntSerializable,就能夠實現經過Network發送int類型的數據了:

Network* network = Network::New();

int i = 1;

network->send(ip, port, IntSerializable(i));

Rust編譯器經過impl關鍵字記錄了每一個類實現了哪些Trait,所以在賦值時編譯器能夠自動實現將對象轉換爲相應的Trait類型,但C++編譯器並無記錄這些轉換信息,須要手動轉換類型。

非侵入式接口讓類和接口區分開來,類中的數據只有成員變量,不包含虛函數表指針,類不會由於實現了N個接口而引入N個虛函數表指針;而接口中只有虛函數表指針,不 包含數據成員,類和接口之間經過實現類進行類型轉換,實現類充當了類與接口之間的橋樑。類只有在充當接口用的時候纔會引入虛函數表指針,不充當接口用的時候沒有虛函數表指針,更符合C++的零開銷原則。

C++編程語言的應用對於開發人員來講是一個很是有用的應用語言。不過其中還有許多比較高深的內容值得咱們去花大量的時間去學習。今天就講到這裏啦,你們記得點贊收藏,分享轉發,加關注哦!

相關文章
相關標籤/搜索