C++作为开发语言的项目一般是系统软件,常见系统级库和系统级应用软件,前者典型如boost,后者很多服务类软件桌面软件。这类工程项目结构一般以编译器编译规则为标准,但不同类型也侧重方面不同。项目的组织形式一般依经验和编译器方便原则有不同的形式。
系统库一般以头文件和编译时确定的模板定义文件为主,没有main函数和很多需编译为独立单元的全局函数,多数执行函数是内联inline函数,且根据库使用时编译条件不同呈现不同编译状态,预先编译的固定部分比较少,这类我们称为头文件集中型项目。
系统应用软件有服务端程序,需要本地运行的客户程序和GUI程序,这类程序由main函数为入口,逐渐展开调用链和调用关系,且函数执行形式和编译时的形式出入不大,在编译时能确定大多数函数形态,这样的程序中类型、函数声明和定义基本分开,定义中基本指明了函数被调用的方式,这类我们称为头文件编译文件平衡型或编译文件为主型。
1. 头文件集中型
头文件为主的C++项目,如类库、模板使用集中项目、内联函数居多项目,组织结构一般多为头文件互包含,其中.h .hpp .inl .ipp文件类型常见,类库供用户使用方式可以为header only,即类库使用者只include头文件即可使用,不用预编译库的二进制动态形式,也不需过多的预配置,随使用的应用程序编译和配置,此外,也可以对类库进行预编译,对固定和复杂函数编译为预编译二进制动态库,实现动态链接的运行空间节省。
现代C++类库和其中的模板定义很难做到声明与定义完全分开,但即使全部实现也定义在头文件,一般也在编译与运行意图上做逻辑上的分离,如boost库的模板定义处理,对模板实现放在ipp文件中,在头文件末尾include模板ipp文件,其他类库也有类似处理,只是名字有的用func.inl,有的用func-inl.h这样的形式。
一般的头文件为主的类库及其模板实现用的文件功能类型和结构常为以下几种:
.h 文件
常规头文件,定义宏或全局类型。c++项目带模板实现的类库比较少用h文件,使用时主要是实现条件编译配置编译参数,如判断系统平台、编译环境、机器架构等.hpp 文件
c++类库常用头文件,定义命名空间中类型,在项目中主要负责放置接口,其中一般只定义类或函数声明和必须实现为内联的函数(可在类声明内,可在类声明外)。
hpp文件包括全部接口声明外,可以定义内联函数的实现,内联函数实现较多时,可以使用二级impl文件夹并在impl下同定义同名hpp文件,放置外部接口hpp中的必须为内联的函数实现同时实现用的一些不对外提供接口的类(声明和实现),impl下的hpp一般在外部hpp结尾include。
有些接口hpp使用的功能中有部分复杂行为过程而外部不需要关注这些,这时可以创建二级detail文件夹在其中定义功能实现细节用的函数、类、其他内容,这些文件一般只给外部接口hpp调用完整功能,这样做到了接口和细节分离。.inl 文件 ipp文件
内联函数文件,这两种文件一般放内联函数和模板实现,包括类的内联成员、全局内联函数、类模板、函数模板、成员模板,由于这些函数都得在编译时知道具体形式,所以一般到使用类库的程序编译时才编译为二进制。
上面的detail文件夹下就可以放置这类文件。
放在inl和ipp中的函数是建议内联的,也就是说和hpp中必须内联的函数不同,这些函数默认内联但最终是否内联由具体情况决定,一般由编译选项决定,选项可设置全编译、部分编译、只头文件三项,在部分编译、只头文件选项中这类函数内联编译,全编译选项这类函数直接编译,inl文件和ipp文件一般在hpp结尾include.imp 文件
这个头文件一般项目中很少有用,为我自己定义,放置建议直接编译的函数,既然有建议内联的函数,就定义建议编译的函数对应,这些函数默认直接编译但最终是否编译由具体情况决定,编译选项为全编译、部分编译时直接编译,选项为只头文件时内联,imp文件在inl或hpp文件结尾include.cpp 文件
直接编译文件,放置必须编译的函数,cpp文件头部include imp、inl、ipp、hpp文件
不同编译选项对应的文件的编译方式和代码形态:
编译目标 .o | 只头文件 | 建议编译 | 全部编译 |
---|---|---|---|
hpp外部接口 | |||
impl/hpp 接口必须内联实现 | |||
impl/inl impl/ipp 建议内联实现 | *** | ||
impl/imp 建议编译实现 | *** | *** | |
cpp 必须编译实现 | ***或不用 | *** | *** |
按文件分类,实现功能和项目组织需要,几类文件组织和项目结构如图
2. 头文件编译平衡型
头文件编译平衡型C++项目一般是实现某应用功能的应用层程序,最后生成形式是可执行文件,非类库,比如系统级的服务器程序,系统守护进程、应用层基础服务,本地GUI程序。这类项目中有需要指定的具体编译实例的函数和变量,作为编译和运行的入口,且载入运行方式固定,如以main函数为调用起始,执行函数调用链,也存在全局变量在main函数中使用,这种实现方式的工程是头文件和直接编译文件平衡,或者偏重直接编译,其组织方式有几个特点。
- 严格说任何编译单元都可以实现为header only只头文件,除main函数在的编译单元。实现方式是:类和结构体声明定义可同时可分开,但成员函数全部定义实现为inline;全局函数全部实现为inline;全局变量全部定义函数局部静态变量,在类静态inline函数中或全局inline函数中返回。全部内容都放入头文件,或分开的文件在头文件适当位置都#include。每个使用这个编译单元的文件都只#include这个头文件。
这样header only方式的效果是编译时需要的时间和复杂度会增加,因为会识别全部类型细节,判断函数形式,决定内联规则,编译并内部或外部链接,但是编译后的代码执行效率会有很大几率提升,因为有编译提供的全部细节供优化,如果编译器取舍得当,执行效率会比全部外部链接快。但也不是全部需要内联,有些函数比较长执行指令较复杂的,不必要内联,肯定会外部链接,因为C++ 11后inline关键字表示由编译器取舍内联规则,不再是一定内联,这时对于编译器判断后仍然外部链接的部分,是header only还是直接编译外部链接效果一样,但是header only会加长编译时间。 - 全部直接编译为全局非内联函数是编译速度最快的,二进制代码量也会小,但是运行效率可能会有影响,比如很短的函数也外部链接,频繁调用跳转开销会占用不少。
- 部分inline,部分外部链接方式比较恰当,但是要根据具体代码功能和函数运行预期,具体确定哪些用哪种方式才能决策合适。
这类项目组织方式和文件结构,按清晰明确原则,不用再设置4-5层的类库细节结构,此类项目分3个主要部分比较恰当,头部声明、建议内联、建议编译三部分,编译选项通过编译时宏条件判断文件组织和代码形态
编译选项不同于类库,比较明确指定编译预期,有4种分别是全部编译、建议编译使用声明、建议编译使用声明和内联、只头文件
3种文件类型后有4种编译选项的组织方式:
全部编译
这时建议编译和建议内联部分全部编译为非inline函数,带头文件。别的编译单元使用时,只使用头部声明。这种方式全部函数都为外部链接。建议编译同时只使用头部声明
这时建议编译部分编译为非inline函数,建议内联部分编译为inline函数,带头文件。别的编译单元使用时,只使用头部声明。这种方式,这个编译单元内的建议编译部分外部链接,建议内联部分内联调用或外部链接,是否内联编译器决策,别的编译单元使用这个编译单元头部声明,这个编译单元inline和非inline函数对其来说都是外部链接同非inline函数。所以效果是本编译单元内有优化,其他编译单元同方式1。建议编译同时使用头部声明和建议内联
这时建议编译部分编译为非inline函数,建议内联部分编译为inline函数,带头文件。别的编译单元使用时,一起使用头部声明和建议内联。这种方式,对本编译单元和别的编译单元,都是建议编译部分外部链接,建议内联部分内联调用或外部链接,是否内联编译器决策。所以效果是本编译单元和其他编译单元都一样有优化。header only只头文件
这时建议内联部分建议编译部分全部函数编译为inline函数。本编译单元和别的编译单元一样,编译时有全部类型信息,编译器可判断优化。所以本编译单元和别的编译单元都有细节提供给编译器优化,优化效果取决于编译器,但是编译时间较长,原因如前述。
头文件编译平衡型,应用项目按功能实现需要,文件组织和代码形态图示: