Google 风格指南

本文是 Google 开源项目风格指南 的简要小结,但是小部分加入了一些我的注释

头文件

  1. 除了 main() 函数和单元测试文件外,每个源文件都应当有对应的头文件

  2. 头文件应该是 自包含 的,所谓 自包含 是指外部引用时不需要为特殊场合包含额外的头文件

  3. 内联函数和模板应当在头文件中添加定义而不是在源文件中

    备注

    • 对于 MSVC 而言,内联函数必须在头文件中实现,否则会编译失败

    • 如果某函数模板为所有相关模板参数显式实例化,或是某个类的私有成员,那么它就只能在源文件中实现

  4. 头文件应当有 #define#prgma once 的保护,以防止头文件被多次包含

    小技巧

    #define 的命名格式为 <PROJECT>_<PATH>_<FILE>_H_ ,其中 <PATH> 相对于项目的源代码路径

  5. 除了本项目的实体外,函数、类模板等应当避免使用 前置声明

    备注

    使用前置声明引入的类是不完全类型,我们只能

    • 使用类的指针或引用

    • 声明 以不完全类型作为参数或返回值的函数

    不完全类型不能用来创建对象或引用其函数。因此一般在头文件中使用前置声明

    不要对标准库中的类使用前置类型声明

  6. 只有函数代码少于十行时才会被定义为内联函数 ( 析构函数递归函数虚函数 不应当定义为内联函数)

  7. #include 应当按照以下顺序包含头文件:

    1. 源文件对应的头文件

    2. C 系统头文件

    3. C++ 系统头文件

    4. 其他库中的头文件

    5. 本项目中的头文件

    6. 按条件包含的头文件

    在每个类别中应当插入空行进行分隔

作用域

  1. 命名空间

    • 源文件 中建议使用 匿名命名空间static 声明 。但是头文件不允许

    • 具名的命名空间其名字应当基于项目名或相对路径

    • 禁止使用 using内敛命名空间

    • 禁止在头文件中使用 命名空间别名

    • 命名空间别名应当只在实现中使用

  2. 函数和变量

    • 非成员函数应当总是放在命名空间中

    • 不要使用类的静态函数模拟命名空间的效果

    • 类的静态方法应当只和类的实例或静态数据相关

    • 变量的作用域应当尽量缩小。只在需要使用变量时才声明它

    • 应当使用初始化的方式声明变量而不是先声明再赋值(当在循环中使用对象时例外)

    • 禁止创建非 POD 类型的 static 变量和全局变量 ( constexpr 变量除外)

    • 禁止使用含有副作用的函数初始化 POD 全局变量

    • 禁止使用函数返回值初始化 POD 变量(除非函数返回值不涉及任何全局变量)

      备注

      上述三条主要是防止在多编译单元中变量初始化顺序的不确定性。但在同一个编译单元内,静态初始化优先于动态初始化,初始化顺序与声明顺序相同。

    • 为了改善静态变量和全局对象析构的不确定性,可以使用 quick_exit() 代替 exit() 终止程序,其不会执行析构过程。

  1. 不要在构造函数中以任何形式(直接调用或通过 init() 间接调用)调用虚函数

  2. 不要在构造函数中执行过多的逻辑相关的初始化

  3. 编译器提供的默认构造函数不会对变量进行初始化

  4. 不要在无法报告错误的情况下在 构造函数 中调用可能会出错的函数

  5. 构造函数的地址无法被获得

  6. 构造函数中不得报告非致命错误

  7. 不要定义隐式类型转换。转换运算符和单参数构造函数应当使用 explicit 修饰

    备注

    如果单参数构造函数没有使用 explicit 那么就无法判断这个函数是用作类型转换还是创建对象的。

  8. 如果不需要类支持拷贝和移动,就把它们删掉。

    这里请参阅 可拷贝类型和可移动类型

  9. 子类重载虚函数时也应该加上 virtual 关键字

  10. 只有仿函数和在仅有数据成员时使用结构体,否则使用类

  11. 除非是公有继承,否则一律使用组合

  12. 当出现多重继承时,最多只能有一个基类是具体类,其他基类必须都为接口类。(除了 部分情况

    备注

    所谓接口类,就是只有纯虚函数和静态函数,没有非静态数据成员,没有定义任何带参数的、非 protect 的构造函数。

    接口类建议使用 Interface 结尾。

  13. 尽量不要重载运算符,也不要创建用户自定义字面量

  14. 没有副作用的二元运算符不应当定义为成员函数

  15. 不要重载 &&||,& 。也不要重载 operator””

  16. 除了 static const 类型外,所有数据成员声明为 private

  17. 控制域应当遵循 public -> protected -> private,将相似的声明放在最后

  18. 存取函数一般内联在头文件中。

    提示

    所谓存取函数,是指存取数据成员的 getter 和 setter 。而不是容器的 push 和 pop

函数

  • 在函数声明时,输入参数(通常由 const 修饰)要放到输出参数(通常是非 const 指针)前面

  • 函数体的长度一般不应该大于 40 行

  • 所有按引用传递的参数必须加上 const

  • 若要使用函数重载, 应当让用户一看函数签名就知道怎么用, 而不是花心思猜测调用的重载函数到底是哪一种。这一规则也适用于构造函数

    备注

    如果函数的参数数量相同,与其重载一个函数(例如 Append() ),不如在函数名上加上参数信息。(比如 AppendString、AppendInt 等)

  1. 禁止在纯虚函数中使用默认参数。在函数重载时 必须 保证默认参数的值相同。

    备注

    对于默认参数而言,以下需要注意:

    • 默认参数时函数重载的另一种语义,一般情况下使用函数重载而不是默认参数

    • 默认参数在每个调用点都需要进行重新求值,这会导致生成的代码迅速膨胀

    • 缺省参数会干扰函数指针, 导致函数签名与调用点的签名不一致 。 而函数重载不会导致这样的问题

    • 如果使用函数指针调用带有默认参数的函数,那么默认参数无效。这是因为默认参数是在运行时求值,而函数调用是编译时。

    • 默认参数值一旦改变,所有设计到函数的文件都必须重新编译

    • 只为可以缺省的参数提供默认值,不要为了调用时省劲就用默认参数 1

    除以下情况外 非常不建议 使用默认参数: 2

    • 源文件中的静态函数或者匿名空间函数

    • 构造函数

    • 用来模拟变长数组

      例如:

      // 通过空 AlphaNum 以支持四个形参
      string StrCat(const AlphaNum &a,
                  const AlphaNum &b = gEmptyAlphaNum,
                  const AlphaNum &c = gEmptyAlphaNum,
                  const AlphaNum &d = gEmptyAlphaNum);
      
  2. 只有在不方便使用前置类型声明时采用后置类型声明

    备注

    C++ 的前置类型声明的方法是:

    int foo(int x);
    

    后置类型写法是:

    auto foo(int x) -> int;
    

    只有在前置类型声明不方便的时候才使用后置类型声明(比如 Lambda 表达式)

  3. 只有在移动构造和移动赋值时才使用右值引用。不要使用 std::forward

  4. 不允许使用变长数组和 alloca()

  5. 不允许使用 C 风格的类型转换

  6. 只在记录日志时使用流,其他时候使用 printfread

    备注

    事实上,流使我们在打印时不在关心对象的类型,但是这也导致用错代码时编译器不会有任何警告。而且流不做特殊处理的时候效率是很低的。尽管我们可以一边用流一边用 printf ,但是根据 一致性原则 ,我们除了日志接口外,其余部分都用 printfread 以统一 IO 接口。

  7. 对于迭代器和其他模板对象而言,只使用前置自增

  8. 尽可能使用 constconstexpr 。 const 建议放在类型前面。

  9. 在声明 const 对象的同时对对象进行初始化。

  10. 关键字 mutable 是线程不安全的,谨慎使用、

  11. 对于整型而言,只使用 intint8_tint64_t 等。(位于 cstdint

  12. 64 位下的可移植性

    备注

    此部分主要针对打印,参见: 64 位下的可移植性

  13. 尽量使用内联函数、枚举和常量代替宏

    • 不要在头文件中定义宏

    • 只有在马上使用时才 #define ,之后立即 #undef

    • 不要对一个已经存在的宏使用 #undef ,选择一个不会冲突的名称

    • 不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为

    • 不要用 ## 处理函数,类和变量的名字

  14. 对于空值而言:整数用 0 ,实数用 0.0 ,指针用 nullptrNULL 、字符用 ‘\0’

  15. 尽量使用 sizeof(变量名) 而不是 sizeof(类型名)

  16. 在保证可读性良好的情况下使用 auto

    • auto 只能用在局部变量里用。别用在文件作用域变量,命名空间作用域变量和类数据成员里。永远别列表初始化 auto 变量。

  17. 使用列表初始化或双括号以避免歧义。使用列表初始化而不是圆括号以避免隐式类型转换。

  18. Lambda 不要捕获全部,只捕获需要的部分

  19. 不要使用过于复杂的模板编程

  20. Boost 只使用以下库:

  • Call Traits : boost/call_traits.hpp

  • Compressed Pair : boost/compressed_pair.hpp

  • <The Boost Graph Library (BGL) : boost/graph, 除了 serialization (adj_list_serialize.hpp) and parallel/distributed algorithms and data structures(boost/graph/parallel/* and boost/graph/distributed/*)

  • Property Map : boost/property_map.hpp

  • The part of Iterator that deals with defining iterators: boost/iterator/iterator_adaptor.hpp, boost/iterator/iterator_facade.hpp, and boost/function_output_iterator.hpp

  • The part of Polygon that deals with Voronoi diagram construction and doesn’t depend on the rest of Polygon: boost/polygon/voronoi_builder.hpp, boost/polygon/voronoi_diagram.hpp, and boost/polygon/voronoi_geometry_type.hpp

  • Bimap : boost/bimap

  • Statistical Distributions and Functions : boost/math/distributions

  • Multi-index : boost/multi_index

  • Heap : boost/heap

  • The flat containers from Container: boost/container/flat_ma*p, and *boost/container/flat_set

  1. friend 是 赋权 语句而不是 声明 语句。友元的函数需要在类外面重新声明一遍

  2. 友元的声明应当与类定义放在一起,定义 可以 放在类的实现文件中。

命名约定

  1. 文件名不要使用大小写命名法,应当使用下划线或者连字符。一般使用下划线

  2. 类型命名应当使用驼峰命名法。不要包含下划线

  3. 变量使用匈牙利命名法,类的成员变量使用下划线结尾,但是结构体的不用

  4. const 或 constexpr 等静态常量或者全局变量以 k 开头,使用驼峰命名法

  5. 函数使用驼峰命名法。存取函数使用匈牙利命名法并要求与变量名匹配。

    备注

    对于字母缩写而言,更倾向于将它们视为一个单词而不是全大写。例如使用 StartRpc() 而不是 StartRPC()

  6. 命名空间以小写字母命名。。

    • 顶级命名空间的名字取决于项目名称

    • 命名空间中的代码, 应当存放于和命名空间的名字匹配的文件夹或其子文件夹中

    • 建议使用更独特的项目标识符 ( websearch::index, websearch::index_util) 而非常见的极易发生冲突的名称 (比如 websearch::util ).

  7. 宏应当使用全大写 + 下划线

  8. 枚举优先采用变量的命名方式,其次也可以采用宏的命名方式

  9. 如果你的实体与 C/C++ 已有实体相似,也可以参考现有命名策略 3

注释

  • 注释使用 ///**/ 都行,统一即可

  • 每个文件开头加入版权公告

  • 如果头文件声明了多个概念,则注释应当为头文件提供一个简要内容。详细的注释应当分散到代码中

  • 除非类的功能非常明显,否则为类提供一份注释以描述其内容和 用法

    如果类的声明和定义分开了, 此时, 描述类用法的注释应当和接口定义放在一起, 描述类的操作和实现的注释应当和实现放在一起.

  • 类的数据成员、全局变量应当加上注释说明其用途。若变量可以接受 NULL-1 等警戒值,也要加以说明

函数注释

  • 函数声明处的注释描述函数功能; 定义处的注释描述函数实现

  • 为函数提供注释,除非它的功能非常明显。注释使用叙述式 (“Opens the file”) 而非指令式 (“Open the file”); 注释只是为了描述函数, 而不是命令函数做什么. 通常, 注释不会描述函数如何工作. 那是函数定义部分的事情。

    一般函数声明处的内容为:

    • 函数的输入输出.

    • 对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.

    • 函数是否分配了必须由调用者释放的空间.

    • 参数是否可以为空指针.

    • 是否存在函数使用上的性能隐患.

    • 如果函数是可重入的, 其同步前提是什么?

  • 如果函数的实现过程中用到了很巧妙的方式, 那么在函数定义处应当加上解释性的注释. 例如, 你所使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由。 举个例子, 你可以说明为什么函数的前半部分要加锁而后半部分不需要。

    小技巧

    不要 从头文件或其他地方的函数声明处直接复制注。 简要重述函数功能是可以的, 但注释重点要放在如何实现上。

  • 对于函数实现较为复杂、晦涩的部分前加上注释

  • 在作用比较奇怪的地方(例如函数返回)后加上注释,注释与代码空两格

  • 函数调用时,在比较晦涩的地方使用枚举等具名变量代替布尔参数。在迫不得已的情况下也可以使用 /**/ 的方式加上注释

  • 不要描述显而易见的代码,不要使用机器翻译作为注释

  • 对于临时的、短期的解决方案,可以使用 TODO

    • TODO 可以加入名字、邮件地址、bug ID 或其他信息

    • TODO 也可以加入截止日期

    // TODO(kl@gmail.com): Use a "*" here for concatenation operator.
    // TODO(Zeke) change this to use relations.
    // TODO(bug 12345): remove the "Last visitors" feature
    
  • 通过 DEPRECATED 注释和关键字 标记某函数的接口已被弃用。 DEPRECATED 后应该留下您的名字, 邮箱地址以及其他身份标识。废弃接口注释还应当引导如何使用此接口的替代方案。

格式

此部分依赖于个人和项目约定,也可参阅 格式

其他内容

以下内容是 Google 为了兼容已有代码或兼容社区成员而考虑的,请酌情考虑是否使用:

  1. 不允许使用异常

  2. 不允许使用 RTTI

1

Google C++ Style Guide 中为什么禁止使用缺省函数参数?

2

缺省参数

3

命名规则的特例