PLC
直播中

王锦霞

7年用户 960经验值
私信 关注

如何去C++实现接口呢

接口分为哪几种?分别有什么作用?
如何去C++实现接口呢?

回帖(1)

李倩

2021-9-22 14:33:59
  前阵子隔壁组来了个Rust开发的架构师,讨论过如何设计方便易用扩展性高的接口。C++不像Java有接口的概念,但是C++可以实现接口的功能。下面就总结一下实际项目工程中实现C++接口的方法。
  接口分为调用接口与回调接口,调用接口主要实现模块解耦的作用,只要保持接口兼容性,模块内部的升级对用户可以做到无感知。良好的接口分层有助于各业务团队高效率开发。
  回调接口主要用于系统有异步事件需要通知用户。系统预定义接口形式,并由用户注册,具体调用时机由系统决定。
  调用接口
  假设有一个网络发送模块类Network,类定义如下:
  class Network
  {
  public:bool send();
  }
  虚函数
  最常用的就是虚函数,可以使用虚函数定义Network接口类:
  class Network
  {
  public: virtual bool send()=0
  static Network* New();
  static void Delete(Network* network_);
  }
  将send定义为虚函数,由继承类去实现(比如由PLC模块或者以太模块继承),以静态方法创建子类对象,以基类Network的指针返回给业务使用。资源遵循谁创建谁销毁的原则,基类还提供Delete方法销毁对象。因为对象销毁封装在接口内部,因此Network接口类可以不需要虚析构函数。
  代码使用虚函数易读性较高,但是虚函数开销较大(需要使用虚函数表指针间接调用),无法在编译期间内联优化,而事实上调用接口在编译期就能确定使用哪个函数,不需要用到虚函数的动态特性。此外由于虚函数使用虚函数表指针间接调用的原因,增加虚函数会导致函数地址表索引变化,新增接口不能在二进制层面兼容老接口。而且由于用户可能继承了Network接口类,在末尾增加虚函数也有风险,因此虚函数接口一旦发布上线就基本无法修改。
  指向实现的指针
  可以使用指向实现的指针来定义Network接口类:
  class NetworkImpl;
  class Network
  {
  public: bool send();
  static Network* New();
  Network()
  ~Network();
  private: NetworkImpl* impl;
  }
  Network的具体实现由NetworkImpl完成,通过使用指向实现的指针的方式来定义接口,接口类对象的创建和销毁可以由用户负责,更好的管理对象生命周期。
  此外该方法通用性强,新增接口不会影响二进制兼容性,有利于项目快速迭代。
  但是该方法还是增加了一层调用,对性能还是略微有影响,不符合C++的零开销原则。
  隐藏的子类
  隐藏的子类思想很简单,接口要实现的目标就是解耦,主要就是隐藏实现,也就是隐藏接口类的成员变量。如果能将接口类的成员变量都转移到另一个隐藏类中,那么接口类就不需要任何成员变量,那么就达到了隐藏实现的目的。具体实现方法如下:
  class Network
  {
  public: bool send();
  static Network* New();
  static void Delete(Network* network_);
  protected: Network();
  ~ Network();
  }
  Network接口类只有成员函数,没有成员变量。提供静态方法New创建对象,Delete方法销毁对象。New方法的实现中会创建隐藏的子类NetworkImol的对象,并以父类Network指针的形式返回。NetworkImol类中定义了Network类的成员变量,并将Network类声明为friend:
  class NetworkImol:public Network
  {
  friend class Network ;
  private:
  // 定义Network类的成员变量
  }
  Network类的实现中创建NetworkImol子类对象,并以父类指针形式返回,通过将this强制转换为子类NetworkImol类型的指针来访问成员变量:
  bool Network::send()
  {
  NetworkImpl* impl = (NetworkImpl*)this;
  //通过impl访问成员变量,实现Network的功能
  }
  static Network* New()
  {
  return new NetworkImpl();
  }
  static Delete(Network* network)
  {
  delete (NetworkImpl*)network;
  }
  该方法符合C++零开销原则,且同样符合二进制兼容性。
  Rust语言中有一种Trait功能,可以在类外面实现一个Trait(不需要修改类代码),那么C++同样可以参考实现Trait功能假设需要在Network类中实现发送序列化数据,重新设计Network接口,Serializable类定义如下:
  class Serializable
  {
  public: virtual void serialize()const =0;
  };
  Network类定义如下:
  class Network
  {
  public: bool send(const char* host, uint16_t port,constSerializable& buf);
  }
  Serializable接口类似于Rust中的Trait,现在任何实现了Serializable接口类的对象都可以通过调用Network类接口完成数据发送功能。那么问题来了,加入项目迭代需要增加通过Network类发送int型数据,如何在不修改类定义的同时实现Serializable接口呢?很简单:
  class IntSerializable :public Serializable
  {
  public:
  IntSerializable(const int i):
  intthis(i){}
  virtual void serialize() const override
  {
  buffer += std::to_string(*intthis);
  }
  private:
  const int* const intthis;
  };
  之后就可以通过Network发送int型数据了:
  Network* network = Network::New();
  int i=1;
  network-》send(ip,port, IntSerializable(i));
  非侵入式接口将类和接口区分开来,类中的数据只包含成员变量,不包含虚函数表指针,因此类不会因为实现了n个接口而引入n个虚函数表指针。而接口中只包含虚函数表指针,不包含数据成员,类和接口之间通过实现类进行类型转换,类只有在充当接口使用的时候才会引入虚函数表指针,不充当接口的时候不会引入,符合C++零开销原则。
  Rust编译器通过impl关键字记录了每个类实现了哪些Trait,因此在赋值时编译器可以自动完成将对象转换为对应的Trait类型。而g++等C++编译器并没有记录这些转换信息,因此需要手动转换类型。本质上还是通过代码帮编译器记录每个接口类实现了哪些Trait,使用模板类的继承,在编译期实现类似“静态多态”的功能。
举报

更多回帖

发帖
×
20
完善资料,
赚取积分