Google C++编码风格

  其实个人觉得技术员工新入职后除了公司规章制度的介绍外,技术族员工第一场培训就应该明确公司的编码规范,并且在项目的管理中要求员工严格遵照执行,比如代码风格不符合规范的提交拒绝合并入主干分支,否则对于个性鲜明的程序员来说,行云流水的代码在指尖不停地产出的同时,也很有可能同时在为公司挖坑埋雷,为后续的项目维护带来巨大隐患。每当在项目中看见花花绿绿、风格迥异的代码,除了让人感觉这是一个甩来甩去的烂尾楼之外,还很有可能把自己带偏了,带有腐臭味道的代码风格像一个毒瘤一样传播开来。面对如此境况,接手的程序员如果不进行重构彻底解决这一尴尬境地,那么接下来避免不了的选择就是:要么选择一个相对较好的风格与之为伍,要么另起炉灶在已有项目中再添加一类代码风格……
  其实因为很多情况下代码风格是非强制性、非技术性问题,所以在极度个性张扬、桀骜不驯的程序员圈子中,代码风格成为管理混乱的重灾区就不足为怪了。程序员除了在项目中具有良好的适应性、可塑性之外,自我养成并坚持一个良好的编码风格还是很有必要的。苦于没有学习的对象?那就照着谷歌Style来吧!
google style

一、头文件

  头文件以.h结尾,通常情况下都是.cc和.h搭配的,除了main.cc和单元测试源代码源文件除外。所有的头文件都应该是self-contain的,即可以作为第一个头文件被引入,而不是后面引入的头文件依赖于前面头文件引入的内容,例外是除了特殊的.inc文件不受此限制,该文件的内容是直接作为文本插入到代码中间位置处的,不需要self-contain,同时也不用ifdef防止重复包含。如果.h文件声明了一个模板或内联函数,那么模板和内联函数的定义就应该同时也在该头文件当中,例外情况为模板函数的所有模板实参都提供了显式实例化,那么其定义就应该放在.cc文件中。
  C/C++所有头文件都应该使用#define防止头文件被重复包含,为了保证唯一性防止潜在的冲突,其宏命名应该基于所在项目源代码树的全路径,即PROJECT_PATH_FILEH\这种类型。同时,在#endif位置处也要使用// PROJECT_PATH_FILEH\表明该endif所对应匹配的#ifdef。
  前置声明是类、函数和模板的纯粹声明而非其定义,使用前置声明的优点有:(a)避免#include展开更多的头文件,节省编译时间;(b)其可以隐藏部分依赖关系,减少重复编译;(c)有些循环依赖的情况还就必须使用前置声明解决。其缺点有:隐藏部分依赖关系会跳过部分代码的编译,会引起错误;前置声明会隐藏类的基础关系,常常会引起错误。因此,对于函数应该总是使用#include包含,而对于类模板,优先使用#include包含。
  内联函数是让编译器展开其代码而非采用通常函数调用机制,对于数据成员的accessor、mutator和一些性能要求较高的代码是很适合的,内联函数代码通常不要超过10行,太长的代码会增加编译文件的长度,降低运行效率。对于函数中带有loop/switch语句的话,这样的代码内联通常增益不大。inline只是对编译器提供提示,有时候编译其会拒绝内联,通常虚函数、递归函数是不会内联的。
  项目中的头文件包含语句中的路径都应该是相对于项目的根目录路径,而不应该使用UNIX风格的相对路径表示。有时候为了测试项目的某个头文件,以及在.cc中包含其所对应的.h文件,可以将该头文件写到源代码路径的顶端,因为这样可以尽早、尽快的让头文件的错误暴露。我们如果在项目中显式需要某个符号而要包含某个头文件,即使我们知道前面所包含的其他.h文件已经隐含包含了该.h,我们还是要显式包含该头文件。
  总体,C++项目包含头文件的顺序遵照:.cc对应的.h、C系统头文件、C++系统头文件、其他库的.h头文件、本项目的.h头文件。各个部分之间使用空行分割区分,而同一个部分下的多个头文件,按照字母顺序排列。

二、作用域

2.1 名字空间

  名字空间是防止发生名字冲突的重要手段,成为又一种垂直封装技术,同时还通过using等手段可以简化命令空间名字的访问。名字空间应该根据项目的名字、路径名信息起名,以防止冲突的发生。禁止使用using指示(using-directive,using namespace std),禁止使用内联命名空间,内联命名空间会自动将内部标识符导出到外层作用域,主要用于保持跨版本ABI兼容性。
  名字空间的命名需要遵守后面的命名规则,在每个命名空间的结尾应该使用// namespace myspace形式的注释。名字空间应该在#include语句,其他名字空间的前置声明等语句之后,封装所有剩余的内容。不要在std名字空间中定义,比如会造成不可移植等潜在问题;禁止使用using指示,这样会污染使用之的命名空间。不要在头文件中使用名字空间别名(namespace bz = ::boo::bar::bz),除非将其置于内部访问的作用域(比如在内联函数中),否则的话其将会被作为一个公共API而被导出。

2.2 匿名名字空间和静态变量

  所有不想被外部链接的名字都可以将其置于匿名名字空间中,而传统上C/C++对于变量、函数也可以声明为static以达到相同的效果,这样各个.cc文件中的同名实体都是项目独立的。
  注意不要在.h文件中使用匿名名字空间和静态变量(感觉之前const变量是被允许的吧)。

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

  任何情况下都需要将非成员函使用名字空间进行包裹,而不应当作为全局函数污染名字空间。某些时候也可以将一系列的函数使用class包裹起来作为类的静态成员函数,但除非这些函数和类成员有着紧密的关系,否则还是推荐前面那种使用名字空间包裹非成员函数的办法进行封装。

2.4 局部变量

  C++允许在函数额任意位置定义变量,推荐变量声明的作用域越窄越好,越接近首次使用位置越好,同时在声明语句中赋予变量初始化值(而不是先声明再赋值),这样使用起来查找变量类型、初始值信息十分方便。
  对于只在if、while、for语句体中使用的变量,必要时候应当在这些语句中进行声明和初始化,这样可以限制对应变量的作用域最小化,例外情况是如果该变量是个类类型,那么和循环语句搭配的时候就需要考虑到变量构造+析构的开销了。

1
2
3
4
5
6
7
8
std::vector<int> v = {1, 2}; 
while (const char* p = strchr(str, '/'))
str = p + 1;

Foo f; // My ctor and dtor get called once each.
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}

2.5 静态和全局变量

  对于类类型的变量,静态声明周期的变量是不允许的,因为其构造和析构的顺序不确定,因此导致的Bug很难跟踪和发现,除非这种类型的变量是constexpr类型的,因为该种类型的变量不会动态创建和析构。对于静态生命周期的变量,包括全局变量、non-local static变量、静态类成员变量、local static变量,都必须是POD(Plain Old Data)类型的,即只允许ints、chars、floats、pointers,以及这些类型所构成的数组或者结构体(所以std::vector和string就不能用了,必须用原始的array和const char[])。
  因为静态变量的初始化顺序是不确定的,甚至相同环境下每次编译都会不同,所以除了上面所说的禁止该种类型的class变量外,允许的变量类型也不可以使用依赖其他全局变量的函数返回值来初始化,例外是local static变量的值可以使用任意函数的返回值来初始化,因为此时的初始化顺序是确定的。程序退出时候析构顺序是跟构造顺序相反的,因此也是不确定的,在复杂的程序中析构顺序的不确定可能会导致严重的后果,处理方式就是使用quick_exit()来终止程序的执行而不是exit(),差异是前者不会调用析构函数、不会调用atexit()注册的处理函数,如果在quick_exit()退出程序时候需要做一些紧急处理(比如刷新日志等)的话,可以使用at_quick_exit()注册处理句柄。
  如果的确需要non-local static或者全局变量的话,可以考虑使用它们的原始指针类型(而非智能指针),在main开始执行的时候初始化它们为有效的地址。

三、类

3.1 构造函数

  不要在构造函数中使用虚函数,因为他们的调用不会发送给派生类的实现,这回导致混乱。构造函数出错很难向外发出错误信号,通常只能通过异常的方式来传递(但是构造函数不应当抛出异常),所以构造函数应当尽可能简洁,通过使用工厂方法或者Init()方式进行构造。

3.2 隐式类型转换

  不要定义隐式类型转换,对转换操作符和单参数的构造函数使用explicit关键字,源类型向目的类型的隐式转换是通过在源类型中定义类型转换操作符实现(operator bool()),而目的类型向源类型的转换是通过目的类型单参数的源类型构造函数来实现的,通过explicit关键字,这些类型的转换只能使用显式转换符才能进行。
  隐式类型除了代码整洁之外,其实弊端很多:他们会隐藏类型不匹配的bug而让使用者难以发觉,隐式类型转换汇同重载函数时候代码会晦涩难懂。当出现单参数的构造函数时候,最好确认作者是允许这种类型的类型转换,还是忘记添加explicit关键字了。
  移动和拷贝构造函数不需要explicit关键字,同时使用std::initializer_list参数的构造函数也不需要explicit关键字,从而允许拷贝初始化操作(MyType m = { 1, 2}; )。

3.3 拷贝和移动操作

  拷贝操作是通过拷贝构造函数和拷贝赋值操作符来实现的,移动操作是通过移动构造函数和移动赋值操作符来实现的,std::unique_ptr是一个典型的可移动但不可拷贝的例子,这些拷贝、移动原语会在某些情况下被隐式自动调用,比如在按值传递的时候。
  拷贝和移动操作的出现,可以让函数按值传递和按值返回,消除了引用、指针对于生命周期的顾虑和原数据修改的危险,而且要想类型同标准容器类的支持,那么拷贝操作是必须支持的。另外拷贝、移动操作的可以由编译器进行检查甚至合成,相比而言比用户自己实现Clone()、Swap()等操作更安全和完整 ,当然也更加的高效。拷贝和移动操作会被自动调用,所以很多时候其调用会被忽略掉,而且大规模的拷贝也可能导致程序的性能问题。
  在实现的时候,如果定义了对应版本的构造函数,那么也要定义对应版本的赋值操作符;如果定义了拷贝构造函数,除非移动操作能够显著得增加效率,否则不要定义移动操作;如果类型是不可拷贝的,到那会移动操作是显然的而且正确的,那么可以定义其移动构造函数和移动赋值操作符。
  如果需要关闭拷贝或者移动操作,应该在类定义的public位置显式将他们定义为=default。

3.4 结构体和类

  struct和class在C++中几乎是相等的,他们的区别跟多是使用习惯上的规范。struct只应该用于封装和保存数据,而不应该有功能性的函数存在。其允许的有限函数也是和数据成员相关的Initialize()、Reset()、Validate()。

3.5 继承和多重继承

  很多情况下Composition相比继承更加合适,如果使用继承的话也要是public继承方式。在C++中,有实现继承和接口继承两种类型,前者会实实在在的继承实现代码,而后者只是函数名被继承了,实现继承会增加代码的复用机会,接口继承强制class暴露特定的API接口。不过,在继承模式下,派生类无法override基类非virtual函数。
  所有的继承都必须是public的,否则的话应该考虑将基类作为派生类的成员变量来使用。项目中应该尽量使用Composition而不是过度使用继承,除非继承的两个类确实是”is-a|is-a-kind-of”的关系类型。
如果有虚成员函数,那么析构函数应该总是virtual的。对于虚函数的派生类覆盖总应当使用override、final关键字进行修饰,这可以帮助编译器在编译的时候检查错误。
  对于C++,只有在很少的情况下多重继承才会被允许,其条件是:在多个基类中只允许有一个基类是实现继承,其余的基类都必须是纯接口派生!

3.6 接口

  接口类可以在类名后缀一个Interface,当一个类被称为纯接口的话,其必须满足的条件有:
  只有纯虚函数( =0 )和静态成员函数;其不允许含有非静态成员;其不需要定义构造函数,即使有构造函数也必须是无参的默认构造函数,且其访问类别是protected;如果其为派生类,则其基类也必须是纯虚接口类。

3.7 操作符重载

  C++中只要有一个参数是用户自定义类型,就允许进行操作符的重载。操作符的重载必须谨慎,禁止用户创造新的操作符语义。
  对于非修改的二元操作符,推荐定义为非成员函数,因为定义为成员函数,隐式转换只能作用于right-hand参数,那么两端参数类型倒换显然就无法适用了;不要为了避免而避免进行操作符重载,如果已经有Equals()、PrintTo()等成员函数的定义,就应该重载对应的操作符;对于标准库的算法、容器往往需要自定义类型支持某些比较操作,此时不应当重载操作符,而是定义comparator比较器作为参数传递给标准库。

3.8 访问控制和声明顺序

  类成员都应该是private的,除非他们是static const类型。
  类成员按照public、protected、private的顺序存放,相似、相关的声明挨着摆放组织,并按照types(typedef、using)、constants、工厂函数、构造函数、赋值操作符、析构函数、其他成员函数、成员变量的顺序摆放。
  不应该在类声明中添加代码比较长的函数定义。

四、函数

4.1 函数参数

  函数参数的定义应该按照先输入、后输出的方式摆放。输入参数通常应该是按值传递或者const-reference方式传递,输出结果应该是non-const pointer的方式传递。这里还需要注意,所有使用引用传递参数都必须是const-reference,但是引用传递具有按值传递的语法但是却有引用传递的语义,为了不至于同按值传递相混淆,引用传参约定为const-reference类型。此处唯一的一个例外是swap()。
  有时候const T*类型优于const T&类型,比如期望可以传递NULL指针的时候,或者函数期望保存传入参数的指针或引用的使用。
  在添加新参数的时候,也需要按照上面的规则进行,不要因为是新添加的参数就将该参数放到最末尾。
  在实现中函数的定义不要太长,一般来说超过40行就考虑是否可以将当前的函数分接成几个部分,以方便后续的维护。

4.2 函数重载

  函数重载虽然让代码更加直观,但是重载过多过复杂的时候,可能会被C++的匹配规则搞晕。还有一个坑点就是在派生类override基类部分虚函数的时候,会隐藏基类同名函数的重载版本,这个时候需要导入基类的名字引入基类的所有重载函数版本。

4.3 默认参数

  默认参数用于参数值大多数都是某个固定值,除非偶然情况需要改变之的情况,是实现函数重载的另外一种手段。默认参数不允许使用于虚函数的情况。
  由于默认参数在每次使用的时候都会重新求值,所以默认参数值不应当具有副作用。

4.4 尾置返回类型

  尾置返回类型不简洁明了,主要用于一些前置返回类型无法使用的情况:比如lambda表达式,或者返回类型依赖于参数类型的情况,即使lambada表达式有些时候编译器可以推断出返回类型,但是使用尾置返回类型会让代码的可读性更好。

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

五、C++特性

5.1 右值引用

  右值引用是绑定在临时对象上的引用类型,推荐在移动构造函数和移动赋值运算符中使用,以及同std::forward进行参数转发的时候使用。
  当使用移动原语和右值引用相结合的时候,可以避免大量的拷贝操作,得到性能的提升;右值引用和std::forward结合,可以进行参数的完美转发,便于写出一些通用的函数封装;对于一些无拷贝语义但是有移动语义的操作,右值引用就可以排上用场了。

5.2 友元

  友元可以使用,通常友元的双方都定义在同一个文件中。一个常用的友元案例就是FooBuilder作为Foo的友元,可以访问Foo的内部状态,对Foo类型进行完整的构造,而不破坏Foo对成员的封装性。

5.3 异常

  C++的异常不要使用。不过有些已有的库使用了异常机制,所以有时候是不可避免的;同时,构造函数传达失败信息的唯一方式就是使用异常机制,虽然可以使用Init()或者工厂方式进行构造,但是使用这种方式增加了对象可能invalid状态的检测和维护。

5.4 运行时类型识别RTTI

  RTTI允许程序员使用typeid或者dynamic_cast操作符可以在程序运行的时候检查对象的类型信息,在项目中应该避免使用RTTI(Run Time Type Information),如果使用到了他大多数情况说明项目的设计和继承关系是有缺陷的。
  RTTI的代码很容易写在if-else、switch的结构中,而如果后续增加了对象可能的类型,那么所有这些位置的代码都需要维护更新,可以用于替代RTTI解决方式的手段有:使用虚函数机制,根据对象实际类型自动改变执行路径;使用Visitor访问者模式,在类的外部决定执行路径。

5.5 类型转换

  应当使用C++风格的cast转换,而避免使用老式C语言的类型转换,前者在形式上更容易被检索和识别,虽然形式上有些啰嗦。数字类型的转换推荐使用初始化列表的语法int64{x},该语法是转换安全的,该形式的转换如果导致信息丢失,会在编译的时候就予以报错。

5.6 流

  流是可移植性的,而且相比printf无须考虑格式化等细节信息,推荐使用在调试输出和测试诊断的情况下。
当使用流的时候,应该禁止使用会修改流状态的操作来设置输出格式,比如imbue()、xalloc()等,而是显示使用函数格式化后再传递给流对象,因为C++中对流对象的设置是持久的,会导致后续的使用不可控。
  流操作也可以方便的将任意类型的数转换成字符串使用。

5.7 前置自增自减

  前置自增自减类型的效率肯定不会比后置版本差,所以如果不使用操作之前的原值,推荐使用前置操作类型。标量类型的变量无所谓,但是对于迭代器类型、模板类等类型,一定要使用前置类型,即使后置操作类型更符合大家的审美习惯。

5.8 const

  无论何时,如果可以都需要使用const修饰符,因为可以指示编译器做对应的检查,除了用于表示变量的值不会修改,其修饰成员函数的时候表明该函数不会修改类的成员变量,只不过在使用中const具有传染性,当调用第三方不规范的代码时候,可能需要对其作出妥协。
  C++11标准的constexpr也是推荐使用的,可以使用真实字面常量甚至函数调用来初始化。使用const变量可以用mutable来对元素作出修改,只不过需要注意线程安全问题。

5.9 整型和64位整型

  int是C++内置的整形类型,如果确定不需要这么大的变量,那么应该使用中的带有长度后缀的整形类型,比如int16_t,而不应该根据当前的系统和环境对各种类型的长度做出假设。对于语言标准中的size_t、ptrdiff_t也是建议使用。
  系统应该尽量避免使用unsigned数据类型比如uint32_t,C/C++的类型提升会让unsigned类型的提升行为变的不那么确定,除非变量适用于位运算等特殊用途,不要为了保证变量不为负而使用unsigned数据类型,在你确认的地方进行assert确认。
  对于32位整形和64位整形,最麻烦的就是C语言的printf族函数,使用不准确的时候会导致编译警告。还有就是对于class/struct类型的补齐问题,特别当需要在硬盘上交换数据的时候不同机器(32位、64位)交换很可能会出现问题,这个时候注意使用编译器的attribute((packed))类设置补齐属性。创建64位整形数值常量的尾部应当使用LL、ULL后缀。

5.10 预处理宏

  不要使用宏,尤其不要在头文件中使用宏。在C++中,宏原来的作用可以使用各种手段进行替换:使用inline函数来优化性能要求的函数调用、使用enum或const来定义常量、使用引用别名等方式也可以简化名字访问。
  因为宏定义下程序员往往看不到宏展开后的代码,所以出现问题很难跟踪调试,还有宏定义是不受名字空间约束的(全局的),所以很容易造成冲突污染。所以在头文件中尽量不要使用宏定义,局部不使用的宏使用#undef即使取消定义,不要使用宏来辅助生成函数、变量名字。项目中各个模块的内的宏,宏的名字需要以包含项目、模块信息已减少名字冲突问题。

5.11 nullptr、sizeof、auto

  对于每种类型,使用它们约定的空值:0、0.0、nullptr(NULL)、’\0’。对于C++11之前的项目可能只能用NULL来代表空指针,不过有些编译器实现可能有些差异致使sizeof(NULL)和sizeof(0)的值不相同。
  sizeof的使用,优先使用sizeof(varname)而不是sizeof(type)。
  auto的使用可以避免那些冗长而又无用的类型字段,反而可以增加代码的可读性,这在C++中大量使用template和namespace的情况下名字长度的问题更为的严重。使用auto还有的一个好处,就是可能避免不必要的类型转换和拷贝操作,因为auto会自动根据初始化值推导出最合适的变量类型。还有一些情况就是代码无法决定值的类型的情况下,就只能用auto顶替了。auto使用的时候要注意auto和const auto&,以免造成不必要的拷贝操作。当对容器迭代的时候,极力推荐使用auto来定义迭代器变量。

5.12 Lambda

  合适的情况下可以使用lambda表达式,作为匿名函数对象可以被方便的传递,比如标准库中的比较原语参数。如果lambda的执行位置会跳出当前的scope,那么就应该使用显式捕获(包括值捕获、引用捕获)参数的方法。随着C++引入std::bind和std::function组件,Lambda和其他可调用对象可以组成通用的回调机制。
  Lambda函数的函数体不应过长,否则应该考虑使用其他机制实现相同的作用效果。

5.13 模板元编程

  避免使用复杂的模板元编程,他们十分的晦涩难懂。

5.14 Boost

  值推荐一些常用的高质量库,其中不少库需要模板元编程的基础才能使用好。

5.15 std::hash

  C++标准库中对于散列容器,散列键允许的数据类型为:所有的整形、所有的浮点型、enum、指针类型、string、智能指针类型等。
  虽然C++教程中让我们可以特地化std::hash类型,但是不建议这么做,因为我们自定义的散列函数分布都不那么好,相应的散列容器性能也不见得那么高。如果非标准类型非要使用散列容器,那么直接使用std::unordered_map类型容器,并提供Key的散列计算。

六、命名规则

  命名必须具有解释性,可以从命名区别出其是类型、变量、函数、常量、宏等信息,虽然命名可以比较的随意,但是最最重要的是要坚持和一致性。
  名字必须具有可解释性,避免为了简介而采用缩写甚至造成歧义,以及在项目之外不被别人所了解熟悉的缩写,除非是dns等这些周知性的缩写。

6.1 文件名

  必须全部是小写字母,并通过-或者字符连接的,其中优先使用字符连接。C++的头文件和源代码文件分别以.h和.cc结尾,而特定的实现代码使用.inc结尾(直接明文插入到相关位置),单元测试的源代码以_test.cc形式结尾,比如foo_bar.h和foo_bar.cc定义和实现了FooBar这个类。

6.2 类型名

  类型名以每个单词的第一个字母大写组成驼峰式命名,其中不包含连接字符,形如:MyExcitingClass、 MyExcitingEnum。所有的class、struct、type、enum、template参数都使用该命名规则。后面把这种方式的命名成为MixedCase命名。

1
2
3
typedef hash_map<UrlTableProperties *, string> PropertiesMap;
using PropertiesMap = hash_map<UrlTableProperties *, string>;
enum UrlTableErrors { ...

6.3 变量名

  变量名包含函数的参数名、类的成员变量名,他们都应该是小写字母并使用进行拼接。class的成员变量(而不是struct的成员变量)还需要结尾添加表示成员变量类型。

1
2
3
4
5
class TableInfo { ...
string table_name_; ...
struct UrlTableProperties {
string name; ...
const int kDaysInAWeek = 7;

  对于const常量,其名字和上面的类型名相同,不过其首部使用k进行常量标识。同时所有静态声明周期的变量(static以及全局变量)也使用这种类型的命名规则。

6.4 函数名

  常规的函数使用MixedCase方式命名,而访问器、修改器类型的函数使用普通变量的命名方式。

1
2
3
int AddTableEntry();
int StartRpc(); //而非StartRPC()
void set_count(int count);

6.5 名字空间名

  所有的namespace名字都是小写字母直接连接,顶层的名字空间命名都和项目名相关,同时名字空间也应该避免直接使用util这类常用的容易冲突的名字。

6.6 枚举名

  枚举值的命令通常有两种类型:kEnumName或者ENUM_NAME,分别称为常量类型和宏类型命名风格。使用宏类型风格的命名是历史原因,新代码应该尽量用常量命名风格。

1
2
3
4
5
enum UrlTableErrors {
kOK = 0,
kErrorOutOfMemory,
kErrorMalformedInput,
};

6.7 宏名字

  不推荐使用,如果使用就该这样:MY_MACRO_THAT_SCARES_SMALL_CHILDREN命名。

七、注释

  虽然注释是个好习惯,但编码的最高境界是自解释的代码,比如体现在使用合理的变量名、函数名风格等。C++的两种注释风格都允许使用,//会更加的常见。
  每个文件应该包含版权信息、作者信息,以及对整个文件代码的简短说明,不过.h和.cc的注释内容不应该相同冗余。
  对于不明显的类,应该注释给予说明其功能,以及尽可能的简短的使用方法,以及注意信息,比如是否可被多线程访问等。
  所有的全局变量都应该有注释,表示其作为什么用途,以及其为什么是全局变量!
对于TODO注释,应当使用一定的风格以便于后续搜索和更新,比如:

1
2
3
// 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进行标示,同时最好也能给出email、作者名等可以检索的符号,同时给出废弃的原因。在C++中,当创建了新接口的时候,通常将原接口包装成inline函数的形式,调用新接口的实现。

八、代码格式

  每行的长度不要超过80个字节,除非是字符串常量等特殊情况。代码的缩进使用空格代替制表符,每个缩进4个字符(原文是2个字符,但是感觉2个字符的缩进层次感不强,要说4个字符缩进多了,用8个字符缩进的还大有人在呢)。

8.1 函数声明和定义

  如果返回类型和函数名能够一行搞定就放在一行,否则返回类型单独一行;函数名、括号、参数之间都没有空格,而括号和花括号之间有一个空格。

1
2
3
4
5
6
7
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
Type par_name1, // 8 space indent
Type par_name2,
Type par_name3) {
DoSomething(); // 4 space indent
...
}

8.2 Lambda表达式

  如果短的话一行解决。参数捕获符号&和变量之间不要有空格。

1
2
3
4
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {
return blacklist.find(i) != blacklist.end();
}),
digits.end());

8.3 函数调用

  函数调用应该尽可能在一行搞定,否则的话调用参数需要对齐。和上面类似,对齐的方式有两种:要么和第一个参数对其,要么从头开始按照调用位置缩进8个字符。可见函数名、括号、参数之间仍然没有空格,各个参数之间使用,和空格分割,参数之间有计算表达式那么参数和表达式之间使用空格分割。

1
2
3
4
5
6
7
bool result = DoSomething(argument1, argument2, argument3);
bool result = DoSomething(averyveryveryverylongargument1,
argument2, argument3);
if (...) {
bool result = DoSomething(
argument1, argument2, // 4 space indent
argument3, argument4 * argument5); ...

8.4 条件语句

  关于if-else的使用,常用的风格是条件表达式和括号之间没有空格分割,虽然保留空格也是可以的。任何时候if、else和括号、花括号之间都必须有空格分割。

1
2
3
4
5
6
7
8
9
10
11
if (condition) {  // no spaces inside parentheses
... // 4 space indent.
} else if (...) { // The else goes on the same line as the closing brace.
...
} else {
...
}
  对于没有else语句的,执行体也只有一个表达式,那么表达式可以和if同行,但如果有else语句则是不允许的。
​```cpp
if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();

  如果if-else的执行体只有一行,通常是允许不使用花括号的,当然也可以使用。不过不允许一个使用一个不使用的情况。

8.5 循环和switch语句

  对于循环中的空执行循环体,使用continue语句进行占位。

1
2
3
4
5
while (condition) {
// Repeat test until it returns false.
}
for (int i = 0; i < kSomeNumber; ++i) {} // Good - one newline is also OK.
while (condition) continue; // Good - continue indicates no logic.

  switch语句中总应该保留default给不匹配的情况,如果确定default不允许存在,则可以在其中assert(false)来断言。

8.6 指针和引用表达式

  在操作指针的时候,、&操作符应该紧接着变量,而声明指针变量和引用变量的时候,可以紧跟类型名也可以紧跟变量名。

1
2
x = *p;  p = &x;
char *c; const string& str;

8.7 布尔表达式

  在结合逻辑与、逻辑或等操作的时候,整个逻辑表达式的长度很可能超过一行,此时所有的逻辑操作符都应该放在行尾部位。

1
2
3
4
5
if (this_one_thing > this_other_thing &&
a_third_thing == a_fourth_thing &&
yet_another && last_one) {
...
}

8.8 类相关

对于类声明,public、private关键字缩进2个字符,其余成员缩进四个字符。而且这些访问修饰符之前都应该留有一个空行,而之后不留空行(紧跟成员声明)。

1
2
3
4
5
class MyClass : public OtherClass {
public: // Note the 2 space indent!
MyClass(); // Regular 4 space indent.
explicit MyClass(int var);
...

类构造函数中的成员初始化列表,接下来一行进行8个字符的缩进。

8.9 其他

  返回表达式一般不需要括号包围来修饰,除非为了返回类型更加的可读性。
  对于预处理指令,即使本身处于缩进代码中,其也必须是顶头写的,而其语句本身可以具有缩进来标示层次。

1
2
3
4
5
6
7
8
MyClass::MyClass(int var) : some_var_(var) {
DoSomething();
}

MyClass::MyClass(int var)
: some_var_(var), some_other_var_(var + 1) {
DoSomething();
}

  小结:文中一些东西是跟C++特性相关的,使用这部分特性主要是为了编写出容易理解,也更易于维护的代码;另外一部分则完全是审美学上的问题,如果觉得自己的代码怎么写都看起来不漂亮的话,就坚持执行这些守则吧!然后,我看Raft的代码写的真是漂亮,大家可以观摩这种风格统一的代码是多么的赏心悦目。

本文完!

参考