Google-Code-Style-C++

some personal notes when reading google code style

头文件

self-contained头文件

头文件应该能够自给自足,self-contained,可以作为第一个头文件被引入

如果.h文件声明了一个模板或者内联函数,同时也在该文件加以定义
例外:如果某函数模板为所有相关模板函数显式实例化,或者本身是某个类的一个私有成员,那么就只能定义在实例化该模板的.cc文件

define保护

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

为保证唯一性,头文件的命名应该基于所在项目源代码树的全路径

前置声明

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

前置声明(forward declaraion)是类、函数和模板的纯粹声明,没有定义

  • 优点
    • 节省编译时间,#include会使代码因为头文件中无关的改动重新编译多次
  • 缺点
    • 隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程;
    • 可能会被库的后续更改破坏
    • 来自命名空间std::的symbol时,行为为定义
    • 很难判断什么时候该用前置声明,什么时候用#include

内联函数

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

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

  • 经验准则
    • 谨慎对待析构函数,析构函数往往比其表面看起来要更长,因为含有隐含的成员和基类析构函数被调用
    • 内联包含循环或者switch语句的函数往往得不偿失
    • 有些函数即使声明为内联也不一定会被编译器内联,比如虚函数和递归函数

#include的路径和顺序

使用标准头文件包含顺序可增强可读性,避免隐藏依赖

依赖的符号(symbol)被哪些头文件定义,就应该包含这些头文件,前置声明情况除外
避免多重包含

作用域

命名空间

鼓励在.cc文件内使用匿名空间或static声明,禁止使用using指示,精致实用内联名空间

命名空间将全局作用域细分为独立的,具名的作用域,可有效防止全局作用域的命名冲突

  • 优点
    • 防止冲突(内联名空间主要用来保持跨版本的ABI兼容性
  • 缺点
    • 命名空间具有迷惑性,使得区分两个相同命名所指代的定义更加困难
    • 完整的命名空间导致代码的冗长
    • 在头文件中使用匿名空间将违背C++的唯一定义原则
  • 结论
    • 遵守命名空间命名中的规则
    • 在命名空间的最后注释出命名空间的名字
    • 不要在命名空间std内声明任何东西,包括标准库的类前置声明
    • 不应该使用using指示引入整个命名空间的标识符号
    • 不要在头文件中使用命名空间别名,除非显式标记内部命名空间使用
    • 禁止使用内联名空间

匿名命名空间和静态变量

在.cc文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为static,但是不要在.h文件中这么做

所有属于匿名命名空间的声明都具有内部链接性,函数和变量可以经由声明为static拥有内部链接性,意味着此文件中声明的标识都不能在另外一个文件中被访问.即使两个文件生命了完全一样名字的标识符,它们所指向的实体实际上时完全不同的

  • 结论
    • 推荐鼓励在.cc中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在.h中使用
    • 匿名命名空间的声明和具名的格式相同,在最后注释上namespace

非成员函数、静态成员函数和全局函数

使用静态成员函数或命名空间内的非成员函数,尽量不要用裸的全局函数
将一系列函数直接置于命名空间中,不要使用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关

优点: 将非成员函数放在命名空间内可避免污染全局作用域

缺点: 将非成员函数和静态成员函数作为新类的成员或许更有意义,当它们需要访问外部资源或具有重要的依赖关系时更是如此

结论: 为了封装若干不共享任何静态数据的静态成员函数而创建类,不如使用命名空间

局部变量

将函数变量尽可能置于最小作用域内,并在变量声明时进行初始化

C++允许在函数的任何位置声明变量,建议离第一次使用越近越好,有一个例外,如果变量是一个对象,每次进入作用域都要调用构造函数,退出作用域都要调用析构函数,这会导致效率降低

静态和全局变量

禁止定义静态存储周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序时不明确的,这将导致代码的不可移植

原生数据类型(POD:Plain Old Data):即int,char和float,以及POD类型的指针,数组和结构体

构造函数的职责

可以在构造函数中进行各种初始化操作,不要在构造函数中调用虚函数,也不要在无法报出错误时进行可能失败的初始化,简单来说,确保在构造函数的行为是确定的

  • 优点

    • 不需要考虑类是否被初始化
    • 经过构造函数初始化后的对象可以为const类型
  • 缺点

    • 构造函数中调用了自身的虚函数,不会重定向到子类的虚函数实现
    • 构造函数很难上报错误
    • 执行失败会得到一个初始化失败的对象,必须使用bool IsValid()机制检查
    • 构造函数的地址无法获取,由构造函数完成的工作是无法以简单的方式交给其他线程
  • 结论

    • 构造函数不允许调用虚函数,或者尝试报告一个非致命错误
    • 可以考虑使用明确的Init()方法或者使用工厂模式

隐式类型转换

不定义隐式类型转换,对于转换运算符和单参数构造函数,使用explicit关键字
explicit关键字可以用于构造函数或类型转换运算符,以保证只有当目的类型在调用点被显式写明时才能进行类型转换
在类型定义中,类型转换运算符和单参数构造函数都应当用explicit进行标记
例外:拷贝和移动构造函数不应当被标记为explicit,因为它们不执行类型转换

可拷贝类型和可移动类型

可拷贝类型允许对象在初始化时得到来自相同类型的另一对象的值, 或在赋值时被赋予相同类型的另一 对象的值, 同时不改变源对象的值.

结构体&类

仅当只有数据成员时使用struct, 其它一概使用class

继承

使用组合比使用继承更合理

子类继承基类时会包含所有数据及操作的定义
使用继承的两种场合:

  • 子类继承父类的实现代码
  • 子类继承父类的方法名称

尽量在”是”的关系下使用继承

多重继承

只在以下情况才推荐使用多重继承:最多只有一个基类是非抽象类

只有当所有父类除第一个外都是纯接口时,才允许使用多重继承

接口

接口是指满足特定条件的类,以Interface为后缀

当一个类满足以下要求时, 称之为纯接口:

  • 只有纯虚函数(“=0”)和静态函数(除了下文提到的析构函数)
  • 没有非静态数据成员
  • 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为protected
  • 如果它是一个子类, 也只能从满足上述条件并以 Interface 为后缀的类继承

运算符重载

除少数特定环境外,不要重载运算符,也不要创建用户定义字面量

存取控制

将所有数据成员声明为private,除非是static const类型成员

声明顺序

将相似的声明放在仪器,将public部分放在最前

函数

参数顺序

输入参数在前,输出参数在后

编写简短函数

倾向于编写简短、凝练的函数,函数超过40行,可以考虑进行分割

引用参数

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

函数重载

如果打算重载一个函数,可以改为在函数名里加上参数信息

缺省参数

》 只允许在非虚函数中使用缺省参数,且必须保证缺省参数的值始终一致

函数返回类型后置语法

》 只有在常规写法(返回类型前置)不便于书写或不便于阅读时使用返回类型后置语法

C++现在允许两种不同的函数声明方式

1
2
int foo(int x);
auto foo(int x) -> int;

优点:

  • 后置返回类型是显式地指定Lambda表达式的返回值的唯一方式
  • 有时在已经出现了的函数参数列表之后指定返回类型,能够让书写更简单,也更易读,尤其是在返回类型依赖于模板参数时

例如

1
2
template <class T, class U> auto add(T t, U u) -> decltype(t + u);
template <class T, class U> decltype(declval<T&>() + declval<U&>()) add(T t, U u);

类型命名

类型名称的每个单词首字母均大写,不包含下划线
所有类型命名–类,结构体,类型定义,枚举,类型模板参数–均使用相同约定

变量命名

变量(包括函数参数)和数据成员名全部小写,单词之间用下划线连接,类的成员变量以下划线结尾,结构体不用

-------------The End-------------