C++ 工程实践(5):避免使用虚函数作为库的接口

本文讨论了作为 C++ 动态库开发者应避免使用虚函数作为接口的原因,指出这可能导致二进制兼容性问题。作者建议使用全局函数、non-virtual 成员函数或 pimpl 技术来提供更稳定和可扩展的接口。此外,文章通过 Linux 系统调用与 COM 接口的对比,强调了接口设计的灵活性和长期维护的重要性。
摘要由CSDN通过智能技术生成

陈硕 (giantchen_AT_gmail)

Blog.csdn.net/Solstice

 

摘要:作为 C++ 动态库的作者,应当避免使用虚函数作为库的接口。这么做会给保持二进制兼容性带来很大麻烦,不得不增加很多不必要的 interfaces,最终重蹈 COM 的覆辙。

本文主要讨论 Linux x86 平台,会继续举 Windows/COM 作为反面教材。

本文是上一篇《C++ 工程实践(4):二进制兼容性》的延续,在写这篇文章的时候,我原本以外大家都对“以虚函数作为接口”的害处达成共识,我就写得比较简略,看来情况不是这样,我还得展开谈一谈。

“接口”有广义和狭义之分,本文用中文“接口”表示广义的接口,即一个库的代码界面;用英文 interface 表示狭义的接口,即只包含 virtual function 的 class,这种 class 通常没有 data member,在 Java 里有一个专门的关键字 interface 来表示它。

C++ 程序库的作者的生存环境

假设你是一个 shared library 的维护者,你的 library 被公司另外两三个团队使用了。你发现了一个安全漏洞,或者某个会导致 crash 的 bug 需要紧急修复,那么你修复之后,能不能直接部署 library 的二进制文件?有没有破坏二进制兼容性?会不会破坏别人团队已经编译好的投入生成环境的可执行文件?是不是要强迫别的团队重新编译链接,把可执行文件也发布新版本?会不会打乱别人的 release cycle?这些都是工程开发中经常要遇到的问题。

如果你打算新写一个 C++ library,那么通常要做以下几个决策:

  • 以什么方式发布?动态库还是静态库?(本文不考虑源代码发布这种情况,这其实和静态库类似。)
  • 以什么方式暴露库的接口?可选的做法有:以全局(含 namespace 级别)函数为接口、以 class 的 non-virtual 成员函数为接口、以 virtual 函数为接口(interface)。

(Java 程序员没有这么多需要考虑的,直接写 class 成员函数就行,最多考虑一下要不要给 method 或 class 标上 final。也不必考虑动态库静态库,都是 .jar 文件。)

在作出上面两个决策之前,我们考虑两个基本假设:

  • 代码会有 bug,库也不例外。将来可能会发布 bug fixes。
  • 会有新的功能需求。写代码不是一锤子买卖,总是会有新的需求冒出来,需要程序员往库里增加东西。这是好事情,让程序员不丢饭碗。

(如果你的代码第一次发布的时候就已经做到完美,将来不需要任何修改,那么怎么做都行,也就不必继续阅读本文。)

也就是说,在设计库的时候必须要考虑将来如何升级

基于以上两个基本假设来做决定。第一个决定很好做,如果需要 hot fix,那么只能用动态库;否则,在分布式系统中使用静态库更容易部署,这在前文中已经谈过。(“动态库比静态库节约内存”这种优势在今天看来已不太重要。)

以下本文假定你或者你的老板选择以动态库方式发布,即发布 .so 或 .dll 文件,来看看第二个决定怎么做。(再说一句,如果你能够以静态库方式发布,后面的麻烦都不会遇到。)

第二个决定不是那么容易做,关键问题是,要选择一种可扩展的 (extensible) 接口风格,让库的升级变得更轻松。“升级”有两层意思:

  • 对于 bug fix only 的升级,二进制库文件的替换应该兼容现有的二进制可执行文件,二进制兼容性方面的问题已经在前文谈过,这里从略。
  • 对于新增功能的升级,应该对客户代码的友好。升级库之后,客户端使用新功能的代价应该比较小。只需要包含新的头文件(这一步都可以省略,如果新功能已经加入原有的头文件中),然后编写新代码即可。而且,不要在客户代码中留下垃圾,后文我们会谈到什么是垃圾。

在讨论虚函数接口的弊端之前,我们先看看虚函数做接口的常见用法。

虚函数作为库的接口的两大用途

虚函数为接口大致有这么两种用法:

  1. 调用,也就是库提供一个什么功能(比如绘图 Graphics),以虚函数为接口方式暴露给客户端代码。客户端代码一般不需要继承这个 interface,而是直接调用其 member function。这么做据说是有利于接口和实现分离,我认为纯属脱了裤子放屁。
  2. 回调,也就是事件通知,比如网络库的“连接建立”、“数据到达”、“连接断开”等等。客户端代码一般会继承这个 interface,然后把对象实例注册到库里边,等库来回调自己。一般来说客户端不会自己去调用这些 member function,除非是为了写单元测试,模拟库的行为。
  3. 混合,一个 class 既可以被客户端代码继承用作回调,又可以被客户端直接调用。说实话我没看出这么做的好处,但实际中某些面向对象的 C++ 库就是这么设计的。

对于“回调”方式,现代 C++ 有更好的做法,即 boost::function + boost::bind,见参考文献[4],muduo 的回调全部采用这种新方法,见《Muduo 网络编程示例之零:前言》。本文以下不考虑以虚函数为回调的过时的做法。

对于“调用”方式,这里举一个虚构的图形库,这个库的功能是画线、画矩形、画圆弧:

   1: struct Point
   2: {
   
   3:   int x;
   4:
评论 110
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值