[C++] 编程实践之1: Google的C++代码风格1:头文件

时间:2022-06-17 19:35:05

  作为使用最为广泛的语言之一,C++也出现在众多开源项目中。为了有效管理C++丰富的语法所带来的复杂性和提高可读性,Google公司专门制定了C++代码风格建议。为了便于自己养成良好的编程习惯,同时为国内同行提供便利,我将原文内容以及YuleFox等人的中文翻译做一个精简版的总结。

头文件

  通常而言,每个.cc(.cpp)文件应该对应一个.h头文件,但也有例外,例如包含main()函数的文件。本小节主要讨论关于头文件的一些代码风格规范。

Self-contained头文件

头文件应该能够自给自足(也就是可以作为第一个头文件被引入),以.h结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应该以.inc结尾。不允许分离出-inl.h头文件的做法。

  所有头文件要能够自给自足。换言之,用户和重构工具不需要为特别场合而包含额外的头文件。详言之,一个头文件要有define保护,统统包含它所需要的其它头文件(这意味着当用户使用某一个.h头文件时,它不再需要包含该头文件所依赖的所有其它非库头文件),也不要求定义任何特别的symbols。
  例外:一个文件并不是self-contained的,而是作为文本插入到代码某处。或者文件内容实际上是其它头文件的特定平台(platform-specific)扩展部分。这些文件就需要用.inc文件扩展名。
  如果.h文件声明了一个模板或者内联函数,同时也在该文件加以定义。凡是有用到这些的.cc文件,就得统统包含该头文件,否则程序可能会在构建中链接失败,不要把这些定义放到分裂的-inl.h文件里。
  例外:如果某函数模板为所有相关模板参数显式实例化,或者本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的.cc文件里。

#define保护

所有头文件都应该用#define来防止头文件被多重包含,命名格式应当是:<PROJECT>_<PATH>_<FILE>_H_.

  为保证唯一性,头文件的命名应该基于所在项目源代码树的全路径,例如项目foo中的头文件foo/src/bar/baz.h的#define保护方式应该为:FOO_BAR_BAZ_H_。

前置声明

尽可能地避免使用前置声明。使用#include包含需要的头文件即可。

定义:

  • 所谓“前置声明”(forward declaration)是类、函数和模板的纯粹声明,没有伴随着其定义。

优点:

  • 前置声明可以节省编译时间,多余的#include会迫使编译器展开更多的文件,处理更多的输入。
  • 前置声明能够节省不必要的重新编译时间。#include使得代码因为头文件中无关的改动而被重新编译多次。

缺点:

  • 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
  • 前置声明可能会被库的后续更改所破坏。前置声明函数或者模板有时候会妨碍头文件开发者变动其API,例如扩大形参类型,加个自带默认参数的模板形参等等。
  • 前置声明来自命名空间std::的symbol时,其行为未定义。
  • 很难判断什么时候该用前置声明,什么时候该用#include。极端情况下,用前置声明代替#include甚至都会暗暗地改变代码的含义:如果#include被B和D的前置声明所代替,test()就会调用f(void*)。前置声明了不少来自头文件的symbol时,就会比单单一行的#include冗长。*仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂。

结论:

  • 尽量避免前置声明那些定义在其它项目中的实体。
  • 函数:总是使用#include。
  • 类模板:优先使用#include。

内联函数

只有当函数只有10行甚至更少时才将其定义为内联函数。

定义:

  • 当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用。

优点:

  • 只要内联的函数体比较小,内联该函数可以令目标代码更加高效。对于存取函数以及气他函数体比较短,性能关键的函数,鼓励使用内联。

缺点:

  • 滥用内联将导致程序变得更慢。内联可能使目标代码量或增或减,这取决于内联函数的大小。内联函数非常短小的存取函数通常会减少代码的大小,但内联一个相当大的函数将戏剧性地增加代码大小。现代处理器由于更好地李永乐指令缓存,小巧的代码往往执行更快。

结论:

  • 一个较为合理的经验准则是:不要内联超过10行的函数。谨慎对待析构函数,析构函数往往比其表面看起来要更长,因为有隐含的成员函数和基类析构函数被调用。
  • 另一个实用的经验准则:内联那些包含循环或者switch语句的函数常常是得不偿失的(除非在大多数情况下,这些循环或者switch语句从不被执行)。
  • 有些函数即使声明为内联也不一定会被编译器内联:比如虚函数递归函数就不会被正常内联。通常递归函数不应该声明成内联函数。(递归调用堆栈的展开并不像循环那么简单,比如递归层数在编译时可能是未知的,大多数编译器都不支持内联递归函数)。

#include的路径及顺序

使用标准的头文件包含顺序可增强可读性,避免隐藏依赖:相关头文件,C库,C++库,其他库的.h,本项目内的.h。

  项目内头文件应该按照源代码目录树结构排列,避免使用UNIX特殊的快捷目录.(当前目录)或者..(上级目录)。
  例如dir/foo.cc的主要作用是实现或者测试dir2/foo2.h的功能,foo.cc中包含头文件的次序如下:
    1. dir2/foo2.h(优先位置)
    2. C系统文件
    3. C++系统文件
    4. 其他库的.h文件
    5. 本项目内.h文件
  这种优顺序保证当dir2/foo2.h遗漏某些必要的库时,dir/foo.cc或者dir/foo_test.cc的构建会立刻终止。因此这一条规则保证维护这些文件的人们首先看到构建终止的消息而不是维护其他包的人们。
  dir/foo.cc和dir2/foo2.h通常位于同一目录下,但也可以放在不同目录下面。
  按照字母顺序对头文件包含进行二次排序是个不错的注意。注意较老的代码可能不符合这条规则,要在方便的时候修正它们。
  您所依赖的symbols被哪些头文件所定义,您就应该包含哪些头文件,forward-declaration的情况除外。比如您要用bar.h中的某个symbol,哪怕您所包含的foo.h已经包含了bar.h,也照样得包含bar.h,除非foo.h有明确说明它会自动向您提供bar.h中的symbol。不过凡是.cc文件所对应的“相关头文件”已经包含的,就不用再重复包含进其.cc文件里面了,就像foo.cc只包含foo.h就够了,不用再管后者所包含的其它内容。
  例外:有时平台特定(system-specific)代码需要条件编译(conditional include),这些代码可以放到其它includes之后,当然平台特定代码也要足够简练并且独立,例如:

#include "foo/public/fooserver.h"

#inlucde "base/port.h" //For LANG_CXX11

#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11

总结

  1. 避免多重包含是学编程的最基本要求。
  2. 前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应。
  3. 内联函数的合理使用可提高代码执行效率。
  4. -inl.h可提高代码的可读性。
  5. 标准化函数参数顺序可以提高可读性和易维护性。
  6. 包含头文件的名称使用比较完整的项目路径看上去很清晰,有条理;包含的次序还可以减少隐藏依赖,使得每个头文件在“最需要编译”的地方编译。头文件都放在对应源文件的最前面,这一点足以保证内部错误的及时发现了。
  7. 类内部的函数一般会自动内联,所以某函数一旦不需要内联,其定义就不要再放在头文件里面了。
  8. 在#include中插入空行以分割相关头文件、C库、C++库,其它库的.h以及本项目内的.h是个好习惯。