你知道的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++编程语言的应用对于开发人员来讲是一个很是有用的应用语言。不过其中还有许多比较高深的内容值得咱们去花大量的时间去学习。今天就讲到这里啦,你们记得点赞收藏,分享转发,加关注哦!

相关文章
相关标签/搜索