使用 Pimpl 技法替代虚函数作为库的接口
C++ 工程实践(5):避免使用虚函数作为库的接口 中详细阐述了使用虚函数做动态库接口在版本升级管理上的诸多弊病,即存在二进制兼容性问题;博主陈硕同学推荐使用 Pimpl Idiom (别称:Handle/Body Idiom) 来替代虚函数作为动态库的接口,下面列举一些关键点。
考虑多采用 non-member non-friend function in namespace 作为接口。
为什么 non-virtual 函数比 virtual 函数更健壮?因为 virtual function 是 bind-by-vtable-offset,而 non-virtual function 是 bind-by-name。加载器 (loader) 会在程序启动时做决议(resolution),通过 mangled name 把可执行文件和动态库链接到一起。就像使用 Internet 域名比使用 IP 地址更能适应变化一样。
pimpl 的基本手法
1. 暴露的接口里边不要有虚函数,而且 sizeof(Graphics) == sizeof(Graphics::Impl*)。
2. 在库的实现中把调用转发 (forward) 给实现 Graphics::Impl ,这部分代码位于 .so/.dll 中,随库的升级一起变化。
3. 如果要加入新的功能,不必通过继承来扩展,可以原地修改,且保持二进制兼容性。先动头文件,然后在实现文件里增加 forward,这么做不会破坏二进制兼容性,因为增加 non-virtual 函数不影响现有的可执行文件。
注:示例代码见原文
采用 pimpl 多了一道 forward 的手续,带来的好处是可扩展性与二进制兼容性,通常是划算的。pimpl 扮演了编译器防火墙的作用。
pimpl 不仅 C++ 语言可以用,C 语言的库同样可以用,一样带来二进制兼容性的好处,比如 libevent2 里边的 struct event_base 是个 opaque pointer,客户端看不到其成员,都是通过 libevent 的函数和它打交道,这样库的版本升级比较容易做到二进制兼容。
陈硕的另外一篇文章也值得认证学习:以boost::function和boost:bind取代虚函数