所谓头文件发布,就是在build某个工程的build过程中,把头文件发布到特定的输出目录,而依赖于此工程的代码,则需要从此特定的输出目录来include头文件。换句话说,在这种做法下,头文件与最终产生的library/binary具有同等地位,它也是build过程的一个产出。
我们写C++代码,一般都是直接从source目录包含所需要的头文件的,那么为什么要使用这种头文件发布的方式呢? 我们可以先分析一下不发布头文件可能带来的问题:
- 因为直接从source目录包含头文件,我们无法控制哪些头文件可以include,而哪些不可以。因为很多情况下,我们很可能只想对用户暴露某个层次的api,但对于用户来说,因为他们在同一目录下,包含任何头文件都是同等方便的。这明显提高了犯错的可能性。
- 如果build不完整,有些问题要到链接时才能发现。
举个例子,如果A依赖于B,你在B中新增加了个函数,在A中使用。然后你在没有build B的情况下直接build了A,这个错误无法在编译期检测出来,而只有把所有源文件编译完了链接的时候你才被通知有这么个错误,对于大工程,这会是个问题。 - 如果build不完整,有些问题在运行时才能发现 - 这就是bug了
举个例子,还是A依赖于B,你在B中修改了一个宏定义或者常量定义,如把#define PI 3.14改成了#define PI 3.1415926,然后你又忘了build B而直接去build A了,此时编译没有问题,但是此时模块B与模块A中对PI的定义就不一致了,必然会造成运行时问题,这种问题要更难发现。
所以,为了保证C++代码中接口与binary的完全统一,包括可见性与行为上的统一,使用头文件发布是非常有效的一个方法,对于上述问题:
- 通过只把需要暴露的头文件发布到特定目录,有效的杜绝了用户”包含不该包含“的头文件的问题
- 因为B中被更新的头文件未被发布,该问题被提前到编译期被发现
- 因为B中被更新的头文件未被发布,此时A与B的binary中使用的PI,都是未更新的3.14,从而保证了一致性。
要实现头文件发布,其实也蛮简单,主要是编译设置上的事,对源代码并没有影响:
为方便说明,还是用A依赖于B为例:
- 在build B时,把B的头文件拷贝到与输出目录bin同级的目录,比如include目录
- 在build A时,把上面提到的include目录作为包含目录
如果你用Visual Studio的话,可以用post build event;用gmake的话,可以新加一个publish header的rule。
当然,如果你有一个高度智能的build system,这个过程可以完全自动化,比如我们team现在实现的一个,只需指明dependency关系,发布头文件,设置包含目录都自动完成,大大简化了build的维护。