如何编写易于移植的C++ 程序

如何编写易于移植的C++ 程序

转自《程序员》2005年第5

/ 紫云英

由于操作系统的差异,同一种操作系统本身版本的差异,目前C++标准库提供的功能仍然有限以及C++编译器产品不是完全兼容等问题,使得我们在移植大型应用程序的时候往往会出现很多难以解决的问题,如何合理的避免他们提高C++程序的移植性,本文作者从源代码的组织安排等方面提出了一些实用的建议。

 

当我们编写服务器端的软件产品时,我们往往需要为同一个软件产品推出多种不同平台版本。这是因为目前还没有哪个服务器操作系统可以一统天下。有不少服务器运行Windows 操作系统,但运行Linux 和各种UNIX操作系统的服务器也很多,而且各种UNIX操作系统之间又有细微的差别。另外,在一些大企业(特别是大银行)中,运行关键业务的服务器往往是IBM 的大型机,它们的操作系统又会和一般的UNIX 有一些不同。

此外,软件依赖的中间件,调用的函数库,要求的编译器,都可看作平台的一部分。上述内容的任意组合会造成大量的可能性。如果平台移植性做得不好,那么很可能软件在你的开发环境能正常运行,但拿到客户的环境中会出现各种奇奇怪怪的问题。

或许你会说,这些都不是问题,用Java来写程序不就一切OK了?不幸的是,有时候一些遗产代码是用C写的,或者你必须依赖的某个关键函数库只提供了C API,经过评估又发现用Java重写,或者通过JNI以及其他可能的跨语言调用机制去封装这些遗产代码或者C API的工作量太大。那么这时候C++往往是更合适的选择。

Java写程序可以跨平台的一大原因是Java有一个无所不包的标准库,而C++的标准库只提供了最基本的一些功能。要用C++写比较大的程序几乎一定会调用到标准库之外的API,而这些API未必可以跨平台。所以,编写易于移植的C++程序要注意的第一点是:如果能有选择,那么尽可能地使用跨平台的API

比如,同样是对文件操作,Win 32 API UNIX 操作系统提供的文件操作函数各不相同,选哪个呢?都不合适,最好还是依赖标准库,fstream或者fopen/fclose都可以。要创建线程并进行线程间同步,Win 32 API UNIX 的做法又不一样。有没有跨平台的解决方案呢?有的,pthreads是跨平台的。如果你的系统需要有对字符串进行操作,是用MFC提供的CString还是标准库中的string呢?显然应该选后者,因为MFC 不是跨平台的。

那么,如果你不得不用到的某些API 没有跨平台的实现,只有各个平台自己的实现,怎么办呢?举个例子,在Windows 平台,加载动态库是调用LoadLibrary;在UNIX平台,加载动态库是调用dlopen。似乎没有什么跨平台的实现。那么我们怎么办?可不可以在每处要加载动态库的地方都这么写?

#ifdef WIN32

HMODULE h = LoadLibrary(libraryname);

#elif defined(UNIX)

int h = dlopen(libraryname, RTLD_LAZY);

#endif

 

不少软件就是这么做的。但这样做很糟糕,因为把平台相关代码同其他的平台独立代码混在了一起,而且代码中会散布很多的#ifdef,影响阅读;而且如果稍后需要把代码移植到另一个平台,那么可能需要修改每一处加载动态库的地方,增加一个#elif defined(),工作量会比较大。

推荐的做法是:自己封装一个跨平台的实现,在平台独立代码中只调用这个跨平台的API,把平台相关性隔离出去。当然,这层封装应该是很薄的,应该只需要用一两行的inline 函数以及几个typedef 即可。这样做的指导思想是,通过封装来增加间接层次,从而把平台独立代码和平台相关代码分离。

下面来看一下,这样做是不是可以了呢?

main.cpp 中(假设我们需要在这个文件中加载动态库)这样写:

#include platform_specific.hpp

int main() {

handle_t h = MyLoadLibrary("libraryname");

// 之后使用动态库,然后卸载

}

 

platform_specific.hpp 中这样写:

#ifdef WIN32

typedef HMODULE /* WIN32 handle type */ handle_t;

inline handle_t MyLoadLibrary(const string& libname)

{

return LoadLibrary(libname.c_str());

}

#elif defined(UNIX)

typedef int /* UNIX handle type */ handle_t;

inline handle_t MyLoadLibrary(const string& libname)

{

return dlopen(libname.c_str(), RTLD_LAZY)

}

#endif

 

这样确实做到了“把平台独立代码和平台相关代码分离”,main.cpp中是平台独立代码,platform_specific.hpp中是平台相关代码,两者分离开来了。移植到新的平台时不需要对main.cpp做任何修改,只需要修改platform_specific.hpp 中的MyLoadLibrary 的实现,而且只要改这一处就可以了。但这样做的一个问题是,platform_specific.hpp 会变得非常混乱,充满了#ifdef。想象一下,除了MyOpenLibrary,可能还会有MyCloseLibraryMyBindSymbol,等等,所有自己封装的跨平台API(也就是实现中需要写#ifdef (某种OS)API)都在里面了。这个文件会变得难以维护,而且很可能是多个人在维护(每个人负责一个不同的平台),修改会非常频繁(特别是如果几个平台的版本同步开发的话)。有没有更好的做法呢?

不妨这样做:在platform_specific.hpp中,只放这些内容:

#ifdef WIN32

#include win32_specific.hpp

#endif

#ifdef UNIX

#include unix_spefic.hpp

#endif

 

而把平台相关的实现部分放在各个平台自己的头文件中去。比如,win32_speific.hpp 是这样的:

typedef HMODULE /* WIN32 handle type */ handle_t;

inline handle_t MyLoadLibrary(const string& libname)

{

return LoadLibrary(libname.c_str());

}

unix_specific.hpp 是这样的:

typedef int /* UNIX handle type */ handle_t;

inline handle_t MyLoadLibrary(const string& libname)

{

return dlopen(libname.c_str(), RTLD_LAZY)

}

 

这样就极大地减少了# i f d e f 的数目。除了在platform_specific.hpp 中会出现#ifdef(需要支持几个平台就有几个),其他所有文件中都不再需要。而且也分离了关注焦点:负责实现平台独立功能的人就专注于编写和维护main.cpp,而负责移植到各个平台的人就编写和维护各自平台的os_specific.hpp。不会造成多人修改同一个文件的冲突,平台独立代码和平台相关代码也得到了很好的分离。

 

有两点值得注意:

第一点,platform_specific.hpp中没有用到#elif,而是用了独立的#ifdef #endif 块。这样做的目的是为了支持下面这样的拓扑结构:

#ifdef WIN32

#include win32_specific.hpp

#endif

#ifdef WINCE

#include wince_specific.hpp

#endif

#ifdef UNIX

#include unix_spefic.hpp

#endif

#ifdef SOLARIS

#include solaris_specific.hpp

#endif

#ifdef AIX

#include aix_specific.hpp

#endif

 

WIN32WINCE不冲突,WINCE是特殊的WIN32SolarisAIX是两种特殊的UNIX,和UNIX也不冲突。如果用了#elif就无法同时#include,但用上面这种拓扑结构就可以做到,而且可以把各个UNIX平台都一样的东西实现在unix_specific.hpp中,而把SolarisAIX有差异的东西实现在solaris_specific.hpp aix_specific.hpp 中,实现进一步的平台细分。

第二点,win32_specific.hppunix_specific.hpp等只能用来封装平台相关的API,不能包含过多的平台独立逻辑。

下面举一个反例:

unix_specific.hpp 中:

int main()

{

// 做平台无关的事情

int h = dlopen(library, RTLD_LAZY);

// 继续做平台无关的事情

}

win32_specific.hpp 中:

int main()

{

// 做平台无关的事情

HMODULE h = LoadLibrary(library);

// 继续做平台无关的事情

}

 

这样做是很不好的。有一部分平台无关代码会被拷贝粘贴,重复出现在了两个地方。拷贝粘贴是编程之大忌。所以一定要注意,那些封装函数只能是很简单的只有一两行的inline 函数,而且不能出现平台独立的代码。

采用这种源文件拓扑结构,可以极大地提高软件的可移植性,而且给编写第一个平台版本带来的麻烦也不大。如果你的开发策略是各个平台同步开发,那么这样做可以让各个平台以及跨平台模块的开发者毫不冲突地工作于不同的源代码文件;如果你的开发策略是先全力发布一个平台的版本,然后移植到另一个平台,那么用这样的源代码结构同样可以给你带来极大的好处:假设第一个版本是Windows 的,稍候发布Linux 版本,那么一开始只有main.

cpp(在这里代表所有的平台独立代码)和win32_specific.hpp。移植的时候只要照着win32_specific.hpp的实现,编写一个linux_specific.hpp 即可。

维护起来也很省心,以后出升级版本或者出patch/servicepack,都只需要在一棵代码树上工作,而没有很多合并修改分支的烦恼。而且还有一个好处是,如果一个bug只在某个平台出现而在其他平台没有,那么找bug基本上只要在那个平台对应的os_specific.hpp中看即可,这是分离关注焦点带来的好处。

正如我前面说过的,平台除了指操作系统,也可以指更广泛的概念,比如中间件或者你依赖的某个第三方库。只要你对平台的依赖是局部性的,而非全局性(比如对框架的依赖),那么这种方法都可适用。我在这里选择了用#ifdef #include配合来选择性地包含和编译平台相关代码。这是通用性最好也最省事的做法,C C++都支持,所有平台上的编译器都支持。当然,还有其他的办法,比如配合使用namespace 定义、using namespace 导入语句、模板的实例化(把操作系统类型作为一个模板参数),也能做到。对预编译器和#号深恶痛绝

的朋友不妨可以试试。

这样的文件结构也可以用于makefile。编译时用make -e OS=YOURTARGETOS [其他参数]来选择性地为某个平台进行构建。其中makefile 应包含这样的内容:

include $(ROOT)/buildenv/default.inc #平台独立的构建信息

include $(ROOT)/buildenv/$(OS).inc #平台相关的构建信息,比如不同平台

#上不同编译器的参数定义

 

因为包含次序在后的宏定义可以覆盖前面的,所以default.inc中还可以为各平台的编译器提供缺省值(比如把编译器缺省定义成cc,有的平台可以覆盖成gcc或者xlC等等;优化参数在default.inc中缺省定义成-O3,在支持更高优化程度的平台.inc 中覆盖成-O5,诸如此类)。宏除了覆盖的话,也可以连接。关于makefile的写法在此限于篇幅就不详述了。事实上还有自动工具(autoconf autoheaderautomake 等)同GNU make配套,可以生成平台相关的文件并进行平台相关构建(具体用法可以通过Google 查找文档),但我觉得很多情况下杀鸡不需要用牛刀除了整体结构,还有很多细节需要注意。比如文件路径分隔符“/”和“/”的不同(boost::path很好地封装了这个不同),这个操作系统的文件系统是否区分大小写,Big EndianLittle Endian 的区分,不同平台上字长的不同,以及不同平台/编译器的缺省对齐方式的不同,等等。另外,要注意一些C++ 编译器提供的API 其实扩展了ANSI 或者ISO 的标准,比如SGI STL 中的hash_maphash_set rope,还有某些C库提供的snprintf 之类函数,这些API 其实不是跨平台的,应避免使用(比如S/390 上的C 库就不带snprintf 函数,绝大部分STL 实现都没有hash_maphash_set rope)。不过如果你觉得使用它们会带来很大方便,也可以用,只是你不得不在不支持这些API 的平台的os_specific.hpp 中自己实现snprintf 或者hash_maprope等等。篇幅所限,这些细节就不展开说了。

 

最后,必须提到,软件应尽可能地具有良好的逻辑和物理设计,这一点非常重要。移植到一个不同的平台,本质上是对软件做修改。设计得越好的软件修改起来越容易。糟糕的设计会导致软件逻辑不清、代码都纠缠在一起,做一点点改动都会牵一发而动全身。这样的软件是很难移植的。而设计得好的软件,对局部做改动不会影响到其余部分,而且一个改动只需要做一次,不需要做全局的查找且替换还担心遗漏一处就造成bug,这样的软件移植起来会很省心。

 

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值