Fan, Quan
Jul 4, 2021
要开新坑了,而且是个对我来说很大的项目,自己一个人完成,开发周期较长,而且期望在学术圈能有很多人用到,所以正式来看看代码规范。Google 的这个代码规范是比较著名的之一。但代码规范并非这一个标准就规定死的,具体使用哪些规则还要看团队的偏好以及既有代码的规范。
本文总结了 Google C++ Style Guide 中比较常用的规则,而且经过简化。如果想查看完整的规范,请移步 原始网页。
头文件
- 通常每一个
.cc文件都应该对应这一个.h文件。除非某些.cc用于单元测试或者仅包含类似main()。 - 头文件要求是自洽的(本身可以通过编译)。
- 一般每个头文件都需要有
header guards,使用#define方式,变量名字使用<PROJECT>_<FULLPATH>_<FILE>_H_以确保绝对的唯一性,例如:#ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif // FOO_BAR_BAZ_H_ - 倾向于将模版定义和内联函数定义和声明放在一起,因为在某些情况下编译器可能无法处理将他们分开的情况。除非你肯定它们只会被当前一小部分代码所使用,则也可以直接在
.cc文件中定义。- 内联函数最好不要超过 10 行。
- 避免在
.cc文件中使用Forward Declarations,即那些只有声明没有定义的情况,典型代表就是extern。 - 按照这个顺序引用头文件:功能上直接相关的头文件、系统头文件、C++ 标准库、其他库、同一个项目中的其他所需头文件。而且不同类别中间有空行间隔。
作用域
- 一般将代码放在一个基于项目名称的独一无二的名字空间中。
- 将非成员函数放在一个单独的名字空间中。并且尽量少使用全局函数
- 不要使用类来“分组”静态函数,类的静态函数应该与类的实例或者相关的静态数据有关。
- 不要使用
using namespace foo这种用法。 - 不要使用内联名字空间。
- 不要在头文件中使用名字空间别名,即
namespace baz = ::foo::bar::baz;,除非定义在只在内部使用的代码块中。 - 如果某些东西不需要被别的文件引用,可以定义到匿名名字空间中或者使用
static,这样别的文件中的同名变量等就会是完全独立的另一个实体。不要在头文件中使用这个用法。 - 虽然 C++ 允许在任何地方声明变量,但我们鼓励在接近第一次使用的位置声明,并最好在声明的时候初始化(而不是先声明,再赋值的方式)。而且推荐多使用初始化列表。
- 只会在
if,while和for语句体中使用的变量可以在这些语句的适当位置进行定义,但某些对象除外(可能会损失性能)。比如:while (const char* p = strchr(str, '/')) str = p + 1;
- 只会在
- 尽量只使用 trivially destructible 类型当做静态/全局变量,以避免构造和析构时可能的互相依赖而导致错误。
- 任何没有声明在函数中的
thread_local变量需要在编译时就进行初始化,以避免部分线程没有初始化的问题。
类
- 不要在构造函数中调用(自己的)虚函数。
- 如果没有很好地抛出异常的手段,可以在构造函数出错时直接终结程序。另外,工厂函数是一个不错的手段。总之要么一切正常,要么一切都没有发生过。
- 给类型转换运算符和有一个参数的构造函数加上
explicit关键字,避免隐式类型转换。但在逻辑上就可以互换的类型、拷贝和转移(move)构造函数、以及接受一个初始化列表的构造函数不用这种方式。 - 暴露给用户的类一定要显示标明拷贝和转移特性,具体方法是定义对应的构造函数,或者使用
delete标记不可用。 - 如果一个结构只需要承载数据,使用
struct,其他的所有情况都是用class,尽管他们在 C++ 中的行为几乎一样。 - 只有在数据成员无法命名的时候才使用
Pair和Tuple,其他情况优先使用struct。 - 可能大多数情况下 Composition(将基类的实例作为数据成员包含在子类中)比继承更合适;尽量不要使用
private和protected继承;多继承的时候最好只继承纯虚类。 - 勤使用
override或者final关键字以避免很多低级错误。 - 只在意义非常明确的时候重载运算符。
- 类的数据成员应该是
private的,常量除外。声明的时候尽量分组书写,将相似的放在一起,而且public的放在前面。建议的声明顺序为:typedef,using,enum,内嵌结构体或者类定义,常量,工厂函数,构造函数,赋值运算符,析构函数,其他函数,最后是数据成员。
函数
- 优先使用
return作为函数输出的手段。 - 不要返回指针,除非允许其为空。
- 必须的输入参数应该使用按值传递或者
const引用/指针,必须的输出参数或者输入/输出参数使用引用/指针传递,其他的使用普通引用/指针传递。
其他 C++ 特性相关
- 只在下面的情况中使用 右值引用: 转移构造函数和转移赋值运算符;需要“消耗掉”目标对象的函数中,目标对象会在之后变为不可用的状态(因为转移了);使用
std::forward的时候;重载函数,与const Foo&搭配。 - 不使用 RTTI。可以使用虚函数,使用 double-dispatch 方法,考虑使用 Visitor 设计模式。
- 尽量不要使用 C 风格的 类型转换,转而使用
static_cast或者初始化列表int64_t y = int64_t{1}。其他的还有const_cast和reinterpret_cast。 - 如果没有特定的需求,使用前置 递增/递减运算符:
++i,--i。 - 在 API 中,多用
const和constexpr。 - 避免使用 宏,尤其是在头文件中,优先使用内联函数、枚举、和常量。尤其不能将宏定义成 API 的样子。如果实在有必要,应该在使用之前定义,并在使用之后取消定义,以及配有详细的注释。也应该避免使用
##生成各种名字。 - 使用
nullptr表示空指针,'\0'表示空字符(而不是0) - 使用
sizeof(varname)而不是sizeof(type),这样更加不容易出错。 - 只在显著提升可读性,或者更加安全的时候使用类型推导功能(比如
auto),不要仅仅因为懒得写类型名字图方便而使用。 - 除非确定模版的实现使用了 模版类型推导 功能,否则不要使用它。
- 使用 lambda 表达式 的时候,尽量显式捕获变量。
- 避免使用过于复杂的 模版编程(Template Metaprogramming)。
命名
- 通用原则:名字的描述性与名字的作用于有关。比如,
n可以被用于五行左右的函数里,但要是放在一个类中,那它的意思就很模糊了。 - 使用
camel case的时候仅仅让首字母大写,比如StartRpc()而不是StartRPC()。 - 文件名:全小写,可以包含下划线
_或者短横线-,更优先使用下划线。 - 类型名:每个单词的首字母大写,无下划线。
- 变量名(包含函数参数和成员变量)全小写,使用下划线连接单词。但对于类的数据成员,要在结尾加一个下划线,比如
a_class_data_member_。 - 常量名:以
k开头,大小写混合,必要时使用下划线连接。 - 函数名:混合大小写,一般以大写开始;setter 和 getter 根据变量名字来命名。
- 名字空间:纯小写,使用下划线分割。最顶级的名字空间使用项目名称命名,子名字空间一般和代码所在目录名保持一致。避免使用同名,尤其是和常见的名字空间重名。
- 枚举变量:同常量。
- 宏变量:首先要避免使用宏,否则使用纯大写,并以下划线分割。
注释
- 可以使用
//或者/* */形式,但要一致。 - 在文件开头加上 license,并描述文件的功能(除非该文件只做了一件事),但要注意
.h和.cc文件的开头注释只保留一处。 - 需要写注释的地方和位置:类(接口定义处)、函数(声明处,如有必要也可以在定义处写)、变量(包含类数据成员)、其他实现上比较值得注意的地方、调用函数时的参数位置。
- 如果有必要在一行的结尾写单行注释,需要间隔两个空格。
- 注意语法、拼写、标点符号的使用等细节。
- 必要时可以使用
TODO注释。
格式化
- 每一行不应该超过 80 个字符,但有例外情况:注释、字符串字面值,include 语句,header guard,using 语句。
- 尽量避免使用非 ASCII 字符,如果要用,必须使用 UTF-8 编码。
- 缩进只使用空格,一次缩进两个空格。
-
函数声明:返回值类型和函数名在同一行,参数尽量也在同一行,如果太长可以换行,但要对齐,例如:
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2, Type par_name3) { DoSomething(); ... } // 如果第一个参数都无法容下 ReturnType LongClassName::ReallyReallyReallyLongFunctionName( Type par_name1, // 4 space indent Type par_name2, Type par_name3) { DoSomething(); // 2 space indent ... }- 如果返回值和函数名无法在一行内容下,则可以换行,但不要有任何缩进
- 左括号一定与函数名字在一起,而且中间没有空格
- 括号和参数之间没有空格
- 左花括号在函数声明的最后一行,而不是下一行的开始
- 右花括号可以单独在一行,或者和左花括号在同一行
- 右括号和左括号之间应该有个空格
- 参数对齐时的缩进是 4 个空格(如果默认缩进是 2 个空格的话)
- Lambda 表达式:语句体类似函数,捕获列表类似参数列表
- 浮点数字面值:要写出小数点而且两边都有数字
- 函数调用:不能写到一行的可以换行,在括号处对齐
- 初始化列表:同函数调用。
- 条件语句:在
if和左括号之间加空格,右括号和左花括号也要有空格,但括号和里面的条件语句之间没有空格。如果其中包含初始话语句,要么放在一行里,要么在分号之后换行。- 例外:如果没有
else或者else if而且语句体只有一行语句,那么可以省略花括号,可以放在一行也可以分开两行。
- 例外:如果没有
- Switch 语句:每个
case的语句体可以使用花括号括起来,但左括号要和case语句在同一行,且右花括号自己占一行。 - 循环语句:单行循环体可以不用花括号,空循环体要使用空的花括号或者
continue语句。 - 指针和引用:点与箭头前后没有空格,
*和&符号后面也没有空格(但前面可以有)。注意风格的一致性。- 允许在同一行声明多个变量,但前提是其中没有指针或者引用类型的。
-
布尔表达式:需要换行的时候要注意换行位置的一致性,下面的例子中都在
&&符号之后换行:if (this_one_thing > this_other_thing && a_third_thing == a_fourth_thing && yet_another && last_one) { ... } - 返回语句:不要使用没有必要的括号。
- 预处理指令:永远顶格,
#之后的空格可有可无。 - 类:
- 以
public,protected,private的顺序写,前后缩进一个空格(默认缩进距离是 2 个空格),前面有空行,但后面不需要空行 - 构造函数的初始化列表:一行或者多行(缩进 4 个空格)
- 以
- 名字空间:里面的语句体没有缩进。
- 空格:根据情况使用,但每行的结尾没有空格。
- 左括号前面
- 分号前没有
- 括号内部紧挨着的地方可有可无,如果有那就在两边都加上
- 类继承时,冒号左右要有空格
- 循环和条件语句:
- 关键字前后要有
- 括号内部紧挨着的位置一般没有空格,但如果有,需要保持风格一致
for循环的分号后面要有,range-based for循环中的冒号前后要有switch语句中冒号前面没有空格,但后面如果有代码则可以加一个空格
- 运算符:
- 赋值运算符左右永远有空格
- 其他二元运算符一般两边也有,但乘除法符号可以没有
- 一元运算符和操作数中间没有空格
- 模版和类型转换:尖括号内部没有空格,
<前面没有空格,>(这两个中间也没有
- 空行:
- 最小化使用量,非不要不要使用。
- 函数之间不要超过一行或者两行
- 函数开头没有,结尾也没有
- 代码“段落”前后可以有。基本哲学是:在一个屏幕中容纳的代码越多,越容易理解控制逻辑。
- 注释之前有个空行可以提升可读性
- 名字空间第一行加空行可以提升可读性,这是个特例。
如果您想表达任何想法,请通过页面底端的联系方式联系我