首先, C++ 也会遇到 "如果直接用了动态库中修改过签名的函数怎么办" 这个全静态语言的千古难题. 而且比起 C 语言更要命的是, C++ 的类是不能进行前置声明来回避对象内存布局耦合: 前置声明就没有引用成员函数了, 那还怎么面向坑爹的对象呢? 不仅仅对象成员布局有此问题, 连同 C++ 藉由实现多态的虚函数亦面临着同样的窘境. 试想动态库中虚函数表都改得面目全非时, 使用该对象的代码却还用原来的方式访问, 那还不是各种鸡飞狗跳.
对于如此恶劣环境下仍然想保持 ABI 兼容性的 C++ 程序员来说, 倒也不是完全无路可走. 具体的方案就不在这篇文章中赘述了, 请转到陈硕老师的 C++ 工程实践: 二进制兼容性和 C++ 工程实践: 避免使用虚函数作为库的接口. 两篇文章都写得非常详细.
撇开这些跟 C 语言有关的孽缘不谈, C++ 本身也提供了一些机制来帮助程序员养成编码恶习, 比如一些函数实现可以直接写进头文件中, 而无须担忧任何链接错误. 首当这个批评的就是成员函数, 包括静态或非静态, 虚或非虚.
inline
函数或者 template
函数实现要写头文件这点可以理解可以忍, 但成员函数来凑什么热闹? 成员函数的不往头文件里面写, 可以避免一些 ABI 耦合, 比如class Date {
int month;
int day;
public:
int getDay() const;
int getMonth() const;
};
Date* date /* initialize */;
date->getDay();
Date::getDay() const
这样的函数签名正如 getday(Date const*)
一样, 这就回到了上一篇文章最后提到的解决方法了. 但是如果用得不恰当, 比如头脑发热认为 inline
一下能提高效率什么的, 而活生生地写成class Date {
int month;
int day;
public:
inline int getDay() const
{
return day;
}
inline int getMonth() const
{
return month;
}
};
inline
函数, 就会理所当然地把代码直接展开嵌入, getDay()
调用就相当于直接访问 Date
类偏移为 sizeof int
的地址. 这样一来, 如果动态库先升级, 在 month
前面再加个 year
程序就必死无疑.inline
函数也不是完全不能使用, 只是这种直接内存访问相关的应该明令禁止. 形如上篇文章中例子里的 void add_person_0(name_t name, age_t age);
和 void add_person_0(name_t name, gender_t gender, age_t age);
两个接口如果想同时使用, 那么这样写是没问题的void add_person(name_t name, gender_t gender, age_t age);
inline void add_person(name_t name, age_t age)
{
add_person(name, gender_t::hideyoshi, age);
}
inline
函数来代替笨拙的默认参数. 当然前提是这个默认参数本身不会造成 ABI 的困扰. (这只是作为独立的例子, 并不表示上篇文章中的问题可以用这个方法解决!)不管怎么说, 将函数体写进头文件这种事情, 对于成员函数或
inline
函数都是可以避免的, 而模板函数则完全无法规避这个大坑. 而这一点, 并不是程序员说 "咱不写模板就好了" 这么简单的事情. 要知道, 这世界上很多函数的实现都是直接往头文件里面堆的, 换句话说, 任何泛型容器, 算法, 都不可以作为有 ABI 需求的接口参数或返回值类型来使用!