接口在程序开发中经常用到。众所周知,在C++语言层面没有接口的概念,但不代表C++不能实现接口的功能。相反,正是因为C++语言没有提供标准的接口,才使得接口的实际实现方式多种多样。那么C++实现接口有哪些方法,不同的方法分别适用于哪些场景呢?本文分享在C++接口工程实践中的一些探索经验。在程序开发过程中经常使用接口。众所周知,在C++语言层面没有接口的概念,但不代表C++不能实现接口的功能。相反,正是因为C++语言没有提供标准的接口,才使得接口的实际实现方式多种多样。那么C++实现接口有哪些方法,不同的方法分别适用于哪些场景呢?本文分享在C++接口工程实践中的一些探索经验。接口的分类接口按功能可分为调用接口和回调接口:调用接口、一段代码、一个模块、一个程序库、一个服务等(以下简称系统),什么对外提供功能,以接口的形式暴露出来之后,用户只需要关心如何调用接口,而不需要关心具体的实现来使用这些功能。这种由用户调用的接口称为调用接口。调用接口的主要作用是实现对用户的解耦和隐藏。用户只需要关心接口的形式,不需要关心具体的实现。只要保持接口的兼容性,实现的修改或升级就不会被用户感知到。解耦后也方便多人合作开发。接口设计好后,各个模块只通过接口进行交互,各自完成自己的模块。回调接口系统定义了一个接口,由用户实现并注册到系统中。当系统需要通知用户一个异步事件时,会回调用户注册的接口的实现。系统定义了界面的形式,而无需关心界面的实现,而是接受用户的注册,并在适当的时候调用。这种由系统定义,用户实现,系统调用的接口称为回调接口。回调接口的主要作用是异步通知。系统定义了通知接口,并在适当的时候发送通知。用户收到通知并执行相应的操作。用户动作执行完毕后,控制权交还给系统,可以将用户动作交给系统。返回一些数据来判断系统后续的行为。2.调用接口我们以一个Network接口为例,来说明C++中调用接口的定义和实现。示例如下:classNetwork{public:boolsend(constchar*host,uint16_tport,conststd::string&message);}网络接口现在只需要一个发送接口就可以发送消息到指定地址。下面我们使用不同的方法来定义网络接口。虚函数虚函数是定义C++接口最直接的方式。使用虚函数定义Network接口类如下:Network*network);}定义send为纯虚函数,让子类实现,子类不对外暴露,提供静态方法New创建子类对象,并将其作为指向父类Network的指针返回。接口设计一般遵循对象在哪里创建就销毁的原则,所以提供了一个静态的Delete方法来销毁对象。因为对象的析构被封装在接口内部,所以Network接口类不需要虚析构函数。使用虚函数定义接口简单明了,但也有很多缺点:虚函数开销:虚函数调用需要使用虚函数表指针间接调用,运行时可以决定调用哪个函数,不能内联优化在编译和链接期间。实际上调用接口可以在编译时决定调用哪个函数,没有虚函数的动态特性。二进制兼容:由于虚函数是根据索引查询虚函数表调用的,添加虚函数会导致索引发生变化,新接口无法在二进制层面兼容旧接口,同时由于用户可能会继承Network接口类,在末尾添加虚函数也有Risk,所以虚函数接口一旦发布,就很难再修改了。指向实现的指针指向实现的指针是在C++中定义接口的推荐方法。使用指向实现的指针来定义网络接口类,如下所示:Network的实现通过impl指针转发给NetworkImpl,NetworkImpl使用预声明对用户隐藏。接口是通过指向实现的指针来定义的。接口类对象的创建和销毁可由用户自行承担。因此,用户可以选择在栈上创建Network类对象,生命周期自动管理。使用指向实现的指针定义接口具有良好的通用性,用户可以直接创建和销毁接口对象,添加新的接口函数不影响二进制兼容性,便于系统演化。指向实现的指针增加了一层调用。虽然对性能的影响几乎可以忽略不计,但不符合C++的零开销原则。那么问题来了,C++能不能实现零开销的接口呢?当然是下面的隐藏子类来介绍。隐藏子类隐藏子类可以实现零开销的接口,思路很简单。调用接口的目的是解耦,主要是隐藏实现,也就是隐藏接口类的成员变量。如果接口类的成员变量可以移到另一个隐藏的实现类中,则接口类不需要任何成员。变量也达到了隐藏执行的目的。隐藏的子类是隐藏的实现类。使用隐藏子类定义Network接口类如下:~Network();}网络接口类只有成员函数(非虚函数),没有成员变量,构造函数和析构函数都被声明为protected。提供静态方法New创建对象,静态方法Delete销毁对象。New方法的实现创建了一个隐藏子类NetworkImpl的对象,并将其作为指向父类Network的指针返回。NetworkImpl类存储Network类的成员变量,将Network类声明为友元:使用返回父类Network指针,通过将this转换为NetworkImpl指针来访问成员变量:boolNetwork::send(constchar*host,uint16_tport,conststd::string&message){NetworkImpl*impl=(NetworkImpl*)this;//通过impl访问成员变量,实现Network}staticNetwork*New(){returnnewNetworkImpl();}staticvoidDelete(Network*network){delete(NetworkImpl*)network;}使用隐藏子类定义接口也有很好的通用性和Binary兼容性,在不增加任何开销的同时,符合C++的零开销原则。三个回调接口也以Network接口为例,说明C++中回调接口的定义和实现。例子如下:classNetwork{public:classListener{public:voidonReceive(conststd::string&message);}boolsend(constchar*host,uint16_tport,conststd::string&message);voidregisterListener(Listener*listener);}现在Network需要添加接收消息的功能,添加Listener接口类,由用户自己实现,将其对象注册到Network中,当有消息到达时,回调Listener方法的onReceive。Virtualfunctions使用虚函数定义Network接口类如下:listener);}定义onReceive为纯虚函数,由用户继承实现。由于多态性的存在,回调是实现类的方法。使用虚函数定义回调接口简单明了,但也有与在调用接口中使用虚函数相同的缺点:虚函数调用开销大??,二进制兼容性差。函数指针函数指针是C语言的方式。使用函数指针定义网络接口类如下:classNetwork{public:typedefvoid(*OnReceive)(conststd::string&message,void*arg);string&message);voidregisterListener(OnReceivelistener,void*arg);}使用函数指针定义C++回调接口简单高效,但只适用于回调接口只有一个回调函数的情况。如果需要在Listener接口类中添加onConnect、onDisconnect等回调方法,用单一的函数指针是无法实现的。另外,函数指针不符合面向对象的思想,可以换成下面要介绍的std::function。std::functionstd::function提供了可调用对象的抽象,它可以封装具有匹配签名的任意可调用对象。使用std::function定义Network接口类如下:);}std::function可以很好地替代函数指针。加上std::bind,具有很好的通用性,因此广受推崇。但是std::function也只适用于回调接口只有一个回调方法的情况。另外std::function比较重量级,但是使用上面的便利会带来性能损失。有人做过性能对比测试,std::function比普通函数慢6倍左右,比虚函数慢。类成员函数指针的使用更加灵活。使用类成员函数指针定义Network接口类如下:主机,uint16_tport,conststd::string&message);voidregisterListener(Listener*listener,OnReceivemethod);template
