FAN, QUAN | 范权

谷歌代码规范中文简版

Fan, Quan
Jul 4, 2021

要开新坑了,而且是个对我来说很大的项目,自己一个人完成,开发周期较长,而且期望在学术圈能有很多人用到,所以正式来看看代码规范。Google 的这个代码规范是比较著名的之一。但代码规范并非这一个标准就规定死的,具体使用哪些规则还要看团队的偏好以及既有代码的规范。

本文总结了 Google C++ Style Guide 中比较常用的规则,而且经过简化。如果想查看完整的规范,请移步 原始网页

头文件

  1. 通常每一个 .cc 文件都应该对应这一个 .h 文件。除非某些 .cc 用于单元测试或者仅包含类似 main()
  2. 头文件要求是自洽的(本身可以通过编译)。
  3. 一般每个头文件都需要有 header guards,使用 #define 方式,变量名字使用 <PROJECT>_<FULLPATH>_<FILE>_H_ 以确保绝对的唯一性,例如:
     #ifndef FOO_BAR_BAZ_H_
     #define FOO_BAR_BAZ_H_
    
     ...
    
     #endif  // FOO_BAR_BAZ_H_
    
  4. 倾向于将模版定义和内联函数定义和声明放在一起,因为在某些情况下编译器可能无法处理将他们分开的情况。除非你肯定它们只会被当前一小部分代码所使用,则也可以直接在 .cc 文件中定义。
    • 内联函数最好不要超过 10 行。
  5. 避免在 .cc 文件中使用 Forward Declarations,即那些只有声明没有定义的情况,典型代表就是 extern
  6. 按照这个顺序引用头文件:功能上直接相关的头文件、系统头文件、C++ 标准库、其他库、同一个项目中的其他所需头文件。而且不同类别中间有空行间隔。

作用域

  1. 一般将代码放在一个基于项目名称的独一无二的名字空间中。
    • 将非成员函数放在一个单独的名字空间中。并且尽量少使用全局函数
    • 不要使用类来“分组”静态函数,类的静态函数应该与类的实例或者相关的静态数据有关。
  2. 不要使用 using namespace foo 这种用法。
  3. 不要使用内联名字空间。
  4. 不要在头文件中使用名字空间别名,即 namespace baz = ::foo::bar::baz;,除非定义在只在内部使用的代码块中。
  5. 如果某些东西不需要被别的文件引用,可以定义到匿名名字空间中或者使用 static,这样别的文件中的同名变量等就会是完全独立的另一个实体。不要在头文件中使用这个用法。
  6. 虽然 C++ 允许在任何地方声明变量,但我们鼓励在接近第一次使用的位置声明,并最好在声明的时候初始化(而不是先声明,再赋值的方式)。而且推荐多使用初始化列表。
    • 只会在 ifwhilefor 语句体中使用的变量可以在这些语句的适当位置进行定义,但某些对象除外(可能会损失性能)。比如:
        while (const char* p = strchr(str, '/'))
            str = p + 1;
      
  7. 尽量只使用 trivially destructible 类型当做静态/全局变量,以避免构造和析构时可能的互相依赖而导致错误。
  8. 任何没有声明在函数中的 thread_local 变量需要在编译时就进行初始化,以避免部分线程没有初始化的问题。

  1. 不要在构造函数中调用(自己的)虚函数。
  2. 如果没有很好地抛出异常的手段,可以在构造函数出错时直接终结程序。另外,工厂函数是一个不错的手段。总之要么一切正常,要么一切都没有发生过。
  3. 给类型转换运算符和有一个参数的构造函数加上 explicit 关键字,避免隐式类型转换。但在逻辑上就可以互换的类型、拷贝和转移(move)构造函数、以及接受一个初始化列表的构造函数不用这种方式。
  4. 暴露给用户的类一定要显示标明拷贝和转移特性,具体方法是定义对应的构造函数,或者使用 delete 标记不可用。
  5. 如果一个结构只需要承载数据,使用 struct,其他的所有情况都是用 class,尽管他们在 C++ 中的行为几乎一样。
  6. 只有在数据成员无法命名的时候才使用 PairTuple,其他情况优先使用 struct
  7. 可能大多数情况下 Composition(将基类的实例作为数据成员包含在子类中)比继承更合适;尽量不要使用 privateprotected 继承;多继承的时候最好只继承纯虚类。
  8. 勤使用 override 或者 final 关键字以避免很多低级错误。
  9. 只在意义非常明确的时候重载运算符。
  10. 类的数据成员应该是 private 的,常量除外。声明的时候尽量分组书写,将相似的放在一起,而且 public 的放在前面。建议的声明顺序为:typedefusingenum,内嵌结构体或者类定义,常量,工厂函数,构造函数,赋值运算符,析构函数,其他函数,最后是数据成员。

函数

  1. 优先使用 return 作为函数输出的手段。
  2. 不要返回指针,除非允许其为空。
  3. 必须的输入参数应该使用按值传递或者 const 引用/指针,必须的输出参数或者输入/输出参数使用引用/指针传递,其他的使用普通引用/指针传递。

其他 C++ 特性相关

  1. 只在下面的情况中使用 右值引用: 转移构造函数和转移赋值运算符;需要“消耗掉”目标对象的函数中,目标对象会在之后变为不可用的状态(因为转移了);使用 std::forward 的时候;重载函数,与 const Foo& 搭配。
  2. 不使用 RTTI。可以使用虚函数,使用 double-dispatch 方法,考虑使用 Visitor 设计模式。
  3. 尽量不要使用 C 风格的 类型转换,转而使用 static_cast 或者初始化列表 int64_t y = int64_t{1}。其他的还有 const_castreinterpret_cast
  4. 如果没有特定的需求,使用前置 递增/递减运算符++i--i
  5. 在 API 中,多用 constconstexpr
  6. 避免使用 ,尤其是在头文件中,优先使用内联函数、枚举、和常量。尤其不能将宏定义成 API 的样子。如果实在有必要,应该在使用之前定义,并在使用之后取消定义,以及配有详细的注释。也应该避免使用 ## 生成各种名字。
  7. 使用 nullptr 表示空指针,'\0' 表示空字符(而不是 0
  8. 使用 sizeof(varname) 而不是 sizeof(type),这样更加不容易出错。
  9. 只在显著提升可读性,或者更加安全的时候使用类型推导功能(比如 auto),不要仅仅因为懒得写类型名字图方便而使用。
  10. 除非确定模版的实现使用了 模版类型推导 功能,否则不要使用它。
  11. 使用 lambda 表达式 的时候,尽量显式捕获变量。
  12. 避免使用过于复杂的 模版编程(Template Metaprogramming)

命名

  1. 通用原则:名字的描述性与名字的作用于有关。比如,n 可以被用于五行左右的函数里,但要是放在一个类中,那它的意思就很模糊了。
  2. 使用 camel case 的时候仅仅让首字母大写,比如 StartRpc() 而不是 StartRPC()
  3. 文件名:全小写,可以包含下划线 _ 或者短横线 -,更优先使用下划线。
  4. 类型名:每个单词的首字母大写,无下划线。
  5. 变量名(包含函数参数和成员变量)全小写,使用下划线连接单词。但对于类的数据成员,要在结尾加一个下划线,比如 a_class_data_member_
  6. 常量名:以 k 开头,大小写混合,必要时使用下划线连接。
  7. 函数名:混合大小写,一般以大写开始;setter 和 getter 根据变量名字来命名。
  8. 名字空间:纯小写,使用下划线分割。最顶级的名字空间使用项目名称命名,子名字空间一般和代码所在目录名保持一致。避免使用同名,尤其是和常见的名字空间重名。
  9. 枚举变量:同常量。
  10. 宏变量:首先要避免使用宏,否则使用纯大写,并以下划线分割。

注释

  1. 可以使用 // 或者 /* */ 形式,但要一致。
  2. 在文件开头加上 license,并描述文件的功能(除非该文件只做了一件事),但要注意 .h.cc 文件的开头注释只保留一处。
  3. 需要写注释的地方和位置:类(接口定义处)、函数(声明处,如有必要也可以在定义处写)、变量(包含类数据成员)、其他实现上比较值得注意的地方、调用函数时的参数位置。
  4. 如果有必要在一行的结尾写单行注释,需要间隔两个空格。
  5. 注意语法、拼写、标点符号的使用等细节。
  6. 必要时可以使用 TODO 注释。

格式化

  1. 每一行不应该超过 80 个字符,但有例外情况:注释、字符串字面值,include 语句,header guard,using 语句。
  2. 尽量避免使用非 ASCII 字符,如果要用,必须使用 UTF-8 编码。
  3. 缩进只使用空格,一次缩进两个空格。
  4. 函数声明:返回值类型和函数名在同一行,参数尽量也在同一行,如果太长可以换行,但要对齐,例如:

     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 个空格的话)
  5. Lambda 表达式:语句体类似函数,捕获列表类似参数列表
  6. 浮点数字面值:要写出小数点而且两边都有数字
  7. 函数调用:不能写到一行的可以换行,在括号处对齐
  8. 初始化列表:同函数调用。
  9. 条件语句:在 if 和左括号之间加空格,右括号和左花括号也要有空格,但括号和里面的条件语句之间没有空格。如果其中包含初始话语句,要么放在一行里,要么在分号之后换行。
    • 例外:如果没有 else 或者 else if 而且语句体只有一行语句,那么可以省略花括号,可以放在一行也可以分开两行。
  10. Switch 语句:每个 case 的语句体可以使用花括号括起来,但左括号要和 case 语句在同一行,且右花括号自己占一行。
  11. 循环语句:单行循环体可以不用花括号,空循环体要使用空的花括号或者 continue 语句。
  12. 指针和引用:点与箭头前后没有空格,*& 符号后面也没有空格(但前面可以有)。注意风格的一致性。
    • 允许在同一行声明多个变量,但前提是其中没有指针或者引用类型的。
  13. 布尔表达式:需要换行的时候要注意换行位置的一致性,下面的例子中都在 && 符号之后换行:

    if (this_one_thing > this_other_thing &&
        a_third_thing == a_fourth_thing &&
        yet_another && last_one) {
    ...
    }
    
  14. 返回语句:不要使用没有必要的括号。
  15. 预处理指令:永远顶格,# 之后的空格可有可无。
  16. 类:
    1. publicprotectedprivate 的顺序写,前后缩进一个空格(默认缩进距离是 2 个空格),前面有空行,但后面不需要空行
    2. 构造函数的初始化列表:一行或者多行(缩进 4 个空格)
  17. 名字空间:里面的语句体没有缩进。
  18. 空格:根据情况使用,但每行的结尾没有空格。
    1. 左括号前面
    2. 分号前没有
    3. 括号内部紧挨着的地方可有可无,如果有那就在两边都加上
    4. 类继承时,冒号左右要有空格
    5. 循环和条件语句:
      1. 关键字前后要有
      2. 括号内部紧挨着的位置一般没有空格,但如果有,需要保持风格一致
      3. for 循环的分号后面要有,range-based for 循环中的冒号前后要有
      4. switch 语句中冒号前面没有空格,但后面如果有代码则可以加一个空格
    6. 运算符:
      1. 赋值运算符左右永远有空格
      2. 其他二元运算符一般两边也有,但乘除法符号可以没有
      3. 一元运算符和操作数中间没有空格
    7. 模版和类型转换:尖括号内部没有空格,< 前面没有空格,>( 这两个中间也没有
  19. 空行:
    1. 最小化使用量,非不要不要使用。
    2. 函数之间不要超过一行或者两行
    3. 函数开头没有,结尾也没有
    4. 代码“段落”前后可以有。基本哲学是:在一个屏幕中容纳的代码越多,越容易理解控制逻辑。
    5. 注释之前有个空行可以提升可读性
    6. 名字空间第一行加空行可以提升可读性,这是个特例。

如果您看不到评论区,请尝试刷新