C++ Style of Google

C++风格规范(Google版)

头文件

Self-contained头文件

所有头文件要能够自给自足。即:用户和重工具不需要为特别场合而包含额外的头文件。

#defined保护

所有头文件都应该使用#define来防止头文件被多重包含,命名格式是<PROJECT>_<PATH>_<FILE>_H
为保证唯一性,头文件的命名应该基于所在项目源代码树的全路径。

前置声明

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

定义

所谓前置声明(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义。

优点:
  • 前置声明能够节省编译时间,多余的#include会迫使编译器展开更多的文件,处理更多的输入。
  • 前置声明能够节省不必要的重新编译的时间。#inclue使代码因头文件中无关的改动而被重新编译多次。
缺点:
  • 前置声明隐依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
  • 前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其API。
  • 前置声明来自命名空间std::的symbol时,其行为未定义。
  • 很难判断什么时候该用前置声明,什么时候该用#include。极端情况下,用前置声明代替inclues甚至都会暗暗地改变代码的含义。
  • 前置声明不少来自头文件的symbol时,就会比单单一行的include冗长。
  • 仅仅为了前置声明而重构代码,会使代码变得更慢更复杂。
结论:
  • 尽量避免前置声明那些定义在其他项目中的实体。
  • 函数:总是使用#include
  • 类模板:优先使用#include

内联函数

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

定义:

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

优点:

只要内联函数体较小,内联函数可以令目标代码更加高效。对于存取函数以及其他函数体比较短,性能关键的函数,鼓励使用内联。

缺点:

滥用内联函数导致程序变得更慢。内联可能使目标代码量或增或减,这取决于内联函数的大小。内联非常短小的存取函数通常会减少代码大小,但内联一个相当大得函数将戏剧性的增加代码大小。现代处理器由于更好的利用了指令缓存,小巧的代码往往执行更快。

结论:
  • 内联函数不要超过10行。谨慎对待析构函数,析构函数往往比其他表面看起来更长,因为有隐含的成员和基类析构函数被调用。
  • 内联那些包含循环或switch语句的函数常常是得不偿失。
    有些函数即使声明为内联的也不一定会被编译内联。必须虚函数和递归函数就不会被正常内联。通常,递归函数不应该声明成内联函数。

#include路径及顺序

使用标准的头文件包含顺序可增强可读性,避免隐藏依赖:相关头文件,C库、C++库,其他库的.h,本项目内的.h。
项目头文件应该按照项目源代码目录树结构排列,避免使用unix特殊的快捷目录.(当前目录)或..(上级目录)。

作用域

命名空间

鼓励在.cc文件使用匿名命名空间或static声明。使用具名的命名空间时,其名称可基于项目名或相对路径。精致使用using表示(using-directive)。禁止使用内联命名空间(inline namespace)。

定义:

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

优点:

虽然类已经提供了命名轴线,命名空间在这基础上又封装了一层。
内联命名空间会自动把内部的标识符放到外层作用域。

缺点:

命名空间具有迷惑性,因为它们使得区分两个相同命名所指代的定义更加困难。
内联命名空间很容易令人迷惑,毕竟其内部的成员不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用。
有时候不得不多次引用某个定义在许多嵌套命名空间里的实体,使用完整的命名空间会导致代码的冗长。
在头文件中使用匿名空间导致违背C++的唯一定义原则(One Definition Rule(ODR))。

结论:
  • 遵守命名空间命名的原则
  • 在命名空间的最后注释出命名空间的名字
  • 用命名空间把文件包含,gflags的声明/定义,以及类的前置声明以外的整个源文件封装起来,1以区别于其它命名空间
  • 不要在命名空间std内声明任何东西,包括标准库的类前置声明。在std命名空间声明实体是未定义的行为,会导致如不可移植。声明标准库下的实体,需要包含对应的头文件。
  • 不应该使用using指示引入整个命名空间的标识符号。
  • 不要在头文件中使用命名空间别名 除非显式标记内部命名空间使用。因为任何在头文件中引入的命名空间都会成为公开API的一部分。
  • 禁止使用内联命名空间。

匿名命名空间和静态变量

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

定义:

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

结论:

推荐、鼓励在.cc中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在.h中使用。

匿名命名空间的声明和具名的格式相同,在最后注释上namespace:

1
2
3
namespace{
...
} //namespace

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

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

优点:

某些情况下,非成员函数和静态成员函数是非常有用的,将非成员函数放在命名空间内可避免污染全局作用域。

缺点:

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

结论:

有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类。

定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的命名空间内.

如果必须定义非成员函数, 又只是在.cc文件中使用它, 可使用匿名命名空间static链接关键字 (如static int Foo() {...}) 限定其作用域.

局部变量

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

C++允许在函数的任何位置声明变量。我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好。 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值。特别是,应使用初始化的方式替代声明再赋值。

1
2
3
4
5
6
7
int j = g(); //初始化声明

vector<int> v;
v.push_back(1); //用花括号初始化更好
v.push_back(1);

std::vector<int> v = {1, 2};

属于if,whilefor语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在这些语句中了。

1
while (const char* p = strchr(str, '/')) strr = p + 1;

注:有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低。

静态和全局变量

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

禁止使用类的静态储存周期变量:由于构造和析构函数调用顺序的不确定性,它们会导致难以发现的 bug 。不过constexpr变量除外,毕竟它们又不涉及动态初始化或析构。

静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体。

静态变量的构造函数、析构函数和初始化的顺序在C++中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化POD变量,除非该函数(比如getenv()getpid())不涉及任何全局变量。函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。

同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从main()返回还是对exit()的调用。析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。比如,在程序结束时某静态变量已经被析构了,但代码还在跑——比如其它线程——并试图访问它且失败;再比如,一个静态 string 变量也许会在一个引用了前者的其它变量析构之前被析构掉。

改善以上析构问题的办法之一是用quick_exit()来代替exit()并中断程序。它们的不同之处是前者不会执行任何析构,也不会执行atexit()所绑定的任何handlers. 如果您想在执行quick_exit()来中断时执行某handler(比如刷新log),您可以把它绑定到_at_quick_exit(). 如果您想在exit()quick_exit()都用上该handler, 都绑定上去。

综上所述,我们只允许POD类型的静态变量,即完全禁用vector(使用C数组替代)和string(使用 const char [])。

如果您确实需要一个class类型的静态或全局变量,可以考虑在main()函数或pthread_once()内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。

类是C++中代码的基本单元。

构造函数的职责

总述

不要在构造函数中调用虚函数,也不要在无法报出错误时进行可能失败的初始化。

定义

在构造函数中可以进行各种初始化操作。

优点
  • 无需考虑类是否被初始化
  • 经过构造函数完全初始化后的对象可以为const类型,也能更方便地被标准容器或算法使用。
缺点
  • 如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向到子类的虚函数实现. 即使当前没有子类化实现, 将来仍是隐患。
  • 在没有使程序崩溃 (因为并不是一个始终合适的方法) 或者使用异常 (因为已经被 禁用 了) 等方法的条件下, 构造函数很难上报错误。
  • 如果执行失败, 会得到一个初始化失败的对象, 这个对象有可能进入不正常的状态, 必须使用bool isValid()或类似这样的机制才能检查出来, 然而这是一个十分容易被疏忽的方法。
  • 构造函数的地址是无法被取得的, 因此, 举例来说, 由构造函数完成的工作是无法以简单的方式交给其他线程的。
结论

构造函数不允许调用虚函数. 如果代码允许, 直接终止程序是一个合适的处理错误的方式。否则, 考虑用Init()方法或工厂函数。

构造函数不得调用虚函数, 或尝试报告一个非致命错误。如果对象需要进行有意义的(non-trivial)初始化, 考虑使用明确的Init()方法或使用工厂模式. Avoid Init() methods on objects with no other states that affect which public methods may be called(此类形式的半构造对象有时无法正确工作)。

隐式类型转换

总述

不要定义隐式类型转换。对于转换运算符和单参数构造函数,请使用explicit关键字。

定义

隐式类型转换允许一个某种类型(源类型)的对象被用于需要另一种类型(目的类型)的位置。
除了语言所定义的隐式类型转换,用户还可以通过在类定义中添加合适的成员定义自己需要的转换。在源类型中定义的隐式类型转换,可以通过目的的类型名的类型转换运算符实现(operator bool())。在目的类型中定义隐式类型转换,泽通过以源类型作为其唯一参数(或唯一无默认值的参数)的构造函数实现。

explicit关键字可以用于构造函数或类型转换运算符,以保证只有当目的类型在调用点被显示写明时才能进行类型转换。例如cast。这不仅用于隐式类型转换,还能作用于C++11的初始化语法:

1
2
3
4
5
class Foo{
explicit Foo(int x, double y);
}

void Func(Foo, f);

优点

  • 有时目的类型名是一目了然的,通过避免显示地写出类型名,隐式类型转换可以让一个类型的可用性和表达性更强。
  • 隐式类型转换可以简单地取代函数重载。
  • 在初始化对象时,列表初始化语法是一种简洁明了的写法。

缺点

  • 隐式类型转换会隐藏不匹配的错误,有时,目的类型并不符合用户的期望,甚至用户根本没有意识到发生了类型转换。
  • 隐式类型转换会让代码难以阅读, 尤其是在有函数重载的时候, 因为这时很难判断到底是哪个函数被调用。
  • 单参数构造函数有可能会被无意地用作隐式类型转换。
  • 如果单参数构造函数没有加上explicit关键字, 读者无法判断这一函数究竟是要作为隐式类型转换, 还是作者忘了加上explicit标记。
  • 并没有明确的方法用来判断哪个类应该提供类型转换, 这会使得代码变得含糊不清。
  • 如果目的类型是隐式指定的, 那么列表初始化会出现和隐式类型转换一样的问题, 尤其是在列表中只有一个元素的时候。
结论

在类型定义中, 类型转换运算符和单参数构造函数都应当用explicit进行标记。一个例外是, 拷贝和移动构造函数不应当被标记为explicit, 因为它们并不执行类型转换。对于设计目的就是用于对其他类型进行透明包装的类来说, 隐式类型转换有时是必要且合适的。这时应当联系项目组长并说明特殊情况.

不能以一个参数进行调用的构造函数不应当加上explicit。接受一个std::initializer_list作为参数的构造函数也应当省略,explicit 以便支持拷贝初始化(例如MyType m = {1, 2};)。

可拷贝类型和可移动类型

总述

如果你的类型需要,就让它们指出拷贝/移动,否则,就把隐式产生的拷贝和移动函数禁用。

定义

可拷贝类型允许对象在初始化时得到来自相同类型的另一对象的值, 或在赋值时被赋予相同类型的另一对象的值, 同时不改变源对象的值。对于用户定义的类型, 拷贝操作一般通过拷贝构造函数与拷贝赋值操作符定义. string类型就是一个可拷贝类型的例子。

可移动类型允许对象在初始化时得到来自相同类型的临时对象的值, 或在赋值时被赋予相同类型的临时对象的值 (因此所有可拷贝对象也是可移动的)。std::unique_ptr<int>就是一个可移动但不可复制的对象的例子。对于用户定义的类型, 移动操作一般是通过移动构造函数和移动赋值操作符实现的。

拷贝 / 移动构造函数在某些情况下会被编译器隐式调用. 例如, 通过传值的方式传递对象。

优点

可移动及可拷贝类型的对象可以通过传值的方式进行传递或者返回, 这使得API更简单, 更安全也更通用。 与传指针和引用不同, 这样的传递不会造成所有权, 生命周期, 可变性等方面的混乱, 也就没必要在协议中予以明确。这同时也防止了客户端与实现在非作用域内的交互, 使得它们更容易被理解与维护。这样的对象可以和需要传值操作的通用API一起使用, 例如大多数容器。

拷贝/移动构造函数与赋值操作一般来说要比它们的各种替代方案, 比如Clone(), CopyFrom() or Swap(), 更容易定义, 因为它们能通过编译器产生, 无论是隐式的还是通过= default。 这种方式很简洁, 也保证所有数据成员都会被复制. 拷贝与移动构造函数一般也更高效, 因为它们不需要堆的分配或者是单独的初始化和赋值步骤, 同时, 对于类似 省略不必要的拷贝 这样的优化它们也更加合适。

移动操作允许隐式且高效地将源数据转移出右值对象. 这有时能让代码风格更加清晰。

缺点

许多类型都不需要拷贝, 为它们提供拷贝操作会让人迷惑, 也显得荒谬而不合理. 单件类型 (Registerer), 与特定的作用域相关的类型 (Cleanup), 与其他对象实体紧耦合的类型 (Mutex) 从逻辑上来说都不应该提供拷贝操作. 为基类提供拷贝 / 赋值操作是有害的, 因为在使用它们时会造成 对象切割。默认的或者随意的拷贝操作实现可能是不正确的, 这往往导致令人困惑并且难以诊断出的错误。

拷贝构造函数是隐式调用的, 也就是说, 这些调用很容易被忽略. 这会让人迷惑, 尤其是对那些所用的语言约定或强制要求传引用的程序员来说更是如此. 同时, 这从一定程度上说会鼓励过度拷贝, 从而导致性能上的问题。

结论

如果需要就让你的类型可拷贝/可移动. 作为一个经验法则, 如果对于你的用户来说这个拷贝操作不是一眼就能看出来的, 那就不要把类型设置为可拷贝。如果让类型可拷贝, 一定要同时给出拷贝构造函数和赋值操作的定义, 反之亦然。如果让类型可拷贝, 同时移动操作的效率高于拷贝操作, 那么就把移动的两个操作 (移动构造函数和赋值操作) 也给出定义。如果类型不可拷贝, 但是移动操作的正确性对用户显然可见, 那么把这个类型设置为只可移动并定义移动的两个操作。

如果定义了拷贝/移动操作, 则要保证这些操作的默认实现是正确的. 记得时刻检查默认操作的正确性, 并且在文档中说明类是可拷贝的且/或可移动的。

由于存在对象切割的风险, 不要为任何有可能有派生类的对象提供赋值操作或者拷贝 / 移动构造函数 (当然也不要继承有这样的成员函数的类). 如果你的基类需要可复制属性, 请提供一个 public virtual Clone() 和一个 protected 的拷贝构造函数以供派生类实现。

如果你的类不需要拷贝/移动操作, 请显式地通过在 public 域中使用 = delete 或其他手段禁用之。

结构体和类

总述

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

说明

在C++中structcalss关键字几乎含义一样。我们为这两个关键字添加我们自己的语义理解,以便为定义的数据类型选择合适的关键字。

struct用来定义包含数据的被动式对象,也可以包含相关的常量,但除了存取数据成员之外,没有别的函数功能。并且存取功能是通过直接访问位域,而非函数调用。除了构造函数、析构函数,InitializeResetValidate等类似的用于设定数据成员的函数,不能提供其它功能的函数。

如果需要更多的函数功能,class更适合。如果拿不准,就用class

为了和STL保持一致,对于仿函数等特性可以不用class而使用struct

注意:类和结构体的成员变量使用不同的命名规则

继承

总述

使用组合常常比使用继承更合适。如果使用继承的话,定义为public继承。

定义

当子类继承基类时,子类包含了父基类所有的数据以及操作的定以。C++实践中,继承主要用于两种场合:实现继承,子类继承父类的实现代码;接口继承,子类仅继承父类的方法名称。

优点

实现继承通过原封不动的复用基类代码减少了代码量。由于继承是在编译时声明,程序员和编译器都可以理解相应操作并发现错误。从编程角度而言,接口继承是用来强制类输出特定的API。在类没有实现API中某个必须的方法,编译器同样会发现并报告错误。

缺点

对于实现继承,由于子类的实现代码散布在父类和子类之间,要理解其实现变得更加困难。子类不能重写父类的非虚函数,当然也就不能修改其实现。基类也可能定义了一些函数成员,因此还必须区分基类的实现布局。

结论

所有继承必须是public。如果你想要使用私有继承,你应该替换成把基类的实例作为成员对象的方式。

不要过度使用实现继承。组合常常更适合一些,尽量做到只在“是一个”(”is-a”)的情况下使用继承:如果bar的确“是一种”Foobar才能继承Foo

必要的话,析构函数声明为virtual。如果你的类有虚函数,则析构函数也应该为虚函数。

对于可能被子类访问的成员函数,不要过度使用protected关键字。注意,数据成员都必须是私有的

对于重载的虚函数或析构函数,使用override,或final关键字显示地进行标记。较早的代码可能使用virtual关键字作为不得已的选项。因此,在声明重载时,请使用overridefinalvirtual中其中之一进行标记。标记为overridefinal的析构函数如果不是对基类虚函数的重载的话,编译会报错。这有助于捕获常见的错误。这些标记起到了文档的作用,因为如果省略这些关键字,代码阅读者不得不检查所有父类,以判断该杉树是否是虚函数。

多重继承

总述

真正需要到多重实现继承的情况少之又少。只在一下情况下才允许多重继承:最多只有一个基类是非抽象类;其他基类都是以Interface后缀的纯接口类

定义

多重继承允许子类拥有多个基类。要将作为纯接口的基类和具有实现的基类区别开来。

优点

相比单继承,多重实现继承可以复用更多的代码。

缺点

真正需要用到多重实现继承的情况少之又少。有时多重实现继承看上去是不错的解决方案,但这是你通常可以找到一个更明确、更清晰的不同解决方案。

结论

只有当所有父类除第一个外都是纯接口类时,才允许使用多重继承。为确保它们是纯接口,这些类必须以Interface为后缀。

注意

关于该规则,Windows下有个特例

接口

总述

接口是指满足特定条件的类,这些类以Interface为后缀(不强制)。

定义

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

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

接口类不能直接被实例化,因为它声明了纯虚函数。为确保接口类的所有实现可被正确销毁,必须为之声明析构函数。

优点

Interface为后缀可以提醒其他人不要为该接口类增加杉树实现或非静态数据成员。这一点对于多重继承尤其重要。另外。对于Java程序员来说,接口的概念已是深入人心。

缺点

Interface后缀增加了类名长度,为阅读和理解带来不便。同时,接口属性作为实现细节不应暴露给用户。

结论

只有在满足上述条件时,类才以Interface结尾,但反过来,满足上述需要类未必一定以Interface结尾。

运算符重载

总述

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

定义

C++允许用户通过使用operator关键字对内建运算符进行重新定义,只要其中一个参数是用户定义的类型。operator关键字还允许用户使用operator""定义新的字面运算符,并且定义类型转换函数,例如operator bool()

优点

重载运算符可以让代码更简洁易懂,也使得用户定义的类型和内建类型拥有相似的行为。重载运算符对于某些运算来说是符合符合语言习惯的名称(例如==<, =, <<),遵循这些语言约定可以让用户定义的类型更易读,也能更好地和需要这些重载运算符的函数库进行交互操作。

对于创建用户定义的类型的对象来说,用户定义字面量是一种非常简洁的标记。

缺点
  • 要提供正确、一致,不出现异常行为的操作符运算需要花费不少精力,而且如果达不到这些要求的话,会导致令人迷惑的bug。
  • 过度使用运算符会带来难以理解的代码,尤其是在重载的操作符的语义与通常的约定不符合时。
  • 函数重载有多少弊端,运算符重载就至少有多少。
  • 运算符重载会混淆试,让你误以为一些好使的操作和操作内建类型一样轻巧。
  • 对重载运算符的调用点的查找需要的可就不仅仅是像grep那样的程序了,这时需要能够理解C++ 语法的搜索工具。
  • 对重载运算符的参数参数写错,此时得到的可能是一个完全不同的重载而非编译错误。例如:
    foo < bar执行的是一个行为,而&foo < &bar执行的就是完全不同的另一个行为了。
  • 重载某些运算符本身就是有害的。例如,重载一元运算符&会导致同样的代码有完全不同的含义,这取决于重载的声明对某段代码而言是否是可见的。重载诸如&&, ||会导致运算顺序和内建运算的顺序不一致。
  • 运算符从通常定义在类的外部,所以对于同一运算,可能出现不同的文件引入了不同的定义的风险。如果两种定义都链接到同一二进制文件,就会导致未定义的行为,有可能表现为难以发现的运行时错误。
  • 用户定义字面量所创建的语义形式对于某些有经验的 C++ 程序员来说都是很陌生的。
结论

只有在意义明显,不会出现奇怪的行为并且与对应的内建运算符的行为一致时才定义重载运算符。例如,|要作为位或或逻辑或来使用,而不是作为shell中的管道。

只有对用户自己定义的类型重载运算符。更准确地说,将它们和它们所操作的类型定义在同一个头文件中,.cc中和命名空间中。这样做无论类型在哪里都能够使用定义的运算符,并且最大程度上避免了多重定义的风险。如果可能的话,请避免将运算符定义为模板,因为此时它们必须对任何模板参数都能够作用。如果你定义了一个运算符,请将其相关且有意义的运算符都进行定义,并且保证这些定义的语义是一致的。例如,如果你重载了<,那么请将所有的比较运算符都进行重载,并且保证对于同一组参数,<>不会同时返回true

建议不要将不进行修改的二元运算符定义为成员函数。如果一个二元运算符被定义为类成员,这时隐式转换会作用域右侧的参数却不会作用于左侧。这时会出现a < b 能够通过编译而b < a不能的情况,这是很让人迷惑的。

不要为了避免重载操作符而走极端。比如说,应当定义 ==, =, 和<<而不是Equals()CopyFrom()PrintTo()。反过来说,不要只是为了满足函数库需要而去定义运算符重载。比如说,如果你的类型没有自然顺序,而你要将它们存入std::set中,最好还是定义一个自定义的比较运算符而不是重载 <

不要重载&&, ||, 或一元运算符&。不要重载 operator"",也就是说,不要引入用户定义字面量。

类型转换运算符在隐式类型转换一节有提及。=运算符在可拷贝类型和可移动类型一节有提及。 运算符<<一节有提及。同时请参见函数重载一节,其中提到的的规则对运算符重载同样适用。

存取控制

总述

所有数据成员声明为private,除非是static const类型成员(遵循常量命名规则)。处于技术上的原因,在使用Google Test时我们允许测试固件类中的数据成员为protected

声明顺序

将相似的声明放在一起,将public部分放在最前。

说明

类定义一般应在public:开始,后跟protected:,最后是private:。省略空部分。

在各个部分中,建议将类似的声明放在一起,并且建议以如下顺序:类型(包括typedefusing和嵌套的结构体与类),常量,工厂函数、构造函数、赋值运算符、析构函数、其他函数,数据成员。

不要将大段的函数定义内联在类定义中。通常,只有那些普通的,或性能关键且短小的函数可以内联在类定义中。参考内联函数一节。

函数

参数顺序

总述

函数的参数顺序为:输入参数在先,后跟输出参数。

说明

C/C++中的函数参数或者函数的输入,或者是函数的输出,或兼而有之。输入参数通常是值参或const引用,输出参数或者输入/输出参数则一般为非const指针。在排列参数顺序时,将所有的输入参数置于输出参数之前。特别注意,在加入新参数时不要因它们是新参数就置于参数列表最后,是仍要按照前述的规则,即将新的输入参数也置于输出参数之前。

这并非一个硬性规定。输入/输出参数(通常是类或结构体)让这个问题变得复杂,并且,有时候为了其他函数保持一致,你可能不得不有所变通。

编写简短函数

总述

我们倾向于编写简短、凝练的函数。

说明

我们承认长函数有时是合理的,因此并不硬性限制函数的长度。如果函数超过40行,可以思索一下能不能在不影响程序结构的前提下对其进行分割。

即使一个长函数现在工作的非常好,一旦有人对其修改,有可能出现新的问题,甚至导致难以发现的 bug。使函数尽量简短,以便于他人阅读和修改代码。

在处理代码时,你可能会发现复杂的长函数。不要害怕修改现有代码:如果证实这些代码使用/调试起来很困难,或者你只需要使用其中的一小段代码,考虑将其分割为更加简短并易于管理的若干函数。

引用参数

总述

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

定义

在C语言中,如果函数需要修改变量的值,参数必须为指针,如int foo(int *pval)。在C++中,函数还可以声明为引用参数:int foo(Interface &val)

优点

定义引用参数可以防止出现(*pval)++这样丑陋的代码。引用参数对于拷贝构造函数这样的应用也是需要的。同时也更明确地不接受空指针。

缺点

容易引起误解,因为引用在语法上是值变量却拥有指针的语义。

结论

函数参数列表中,所有引用参数都必须是const

1
void Foo(const string &in, string *out);

事实上这在Google Code是一个硬性约定:输入参数是值参或const引用,输出参数为指针。输入参数可以是const指针,但绝不能是非const的引用参数,除非特殊要求,比如swap

有时候,在输入形参中用const T*指针比const T&更明智。比如:

  • 可能会传递空指针。
  • 函数要把指针或地址的引用赋值给输入形参。

总而言之,大多时候输入形参往往是const T&。若用const T*则说明输入另有处理。所以若要使用const T*,则应给出相应的理由,否则会使得读者感到迷惑。

函数重载

总述

若要使用函数重载,则必须能让读一看调用点就胸有成竹,而不用花心思猜测调用的重载函数到底是哪一种,这样一规则也适用于构造函数。

定义

你可以编写一个参数类型为const string&的函数,然后用另一个参数类型为const char*的函数对齐进行重载:

1
2
3
4
5
6
class MyClass{
public:
void Analyze(const string &text);
void Analyze(const char* text, size_t textlen);
}
};

优点

通过重载参数不同的同名参数,可以令代码更加直观,模版化代码需要重载,这同时也能为使用这带来便利。

缺点

如果函数单靠不同的参数类型而重载,读者就得十分熟悉C++五花八门的匹配规则,以了解匹配过程具体到底如何。另外,如果派生类只重载了某个函数的部分变体,继承语义的容易令人困惑。

结论

如果打算重载一个函数,可以试试改在函数名里加上参数信息。例如,用AppendString()AppendInt等, 而不是一口气重载多个Append()。如果重载函数的目的是为了支持不同数量的同一类型参数,则优先考虑使用std::vector以便使用者可以用列表初始化指定参数。

缺省参数

总述

只允许在非虚函数中使用缺省参数,且必须保证缺省参数的值始终一致。缺省参数与函数重载遵循同样的规则。一般情况下建议使用函数重载,尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下。

优点

有些函数一般情况下使用默认参数,但有时需要又使用非默认的参数。缺省参数为这样的情形提供了便利,使程序员不需要为了极少的例外情况编写大量的函数。和函数重载相比,缺省参数的语法更简洁明了,减少了大量的样板代码,也更好地区别了“必要参数”和“可选参数”。

缺点

缺省参数实际上是函数重载语义的另一种实现方式,因此所有不应当使用函数重载的理由也都适用于缺省参数。

虚函数调用的缺省参数取决于目标对象的静态类型,此时无法保证给定函数的所有重载声明的都是同样的缺省参数。

缺省参数是在每个调用点都要进行重新求值的,这会造成生成的代码迅速膨胀。作为读者,一般来说也更希望缺省的参数在声明时就已经被固定了,而不是在每次调用时都可能会有不同的取值。

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

结论

对于虚函数,不允许使用缺省参数,因为在虚函数中缺省参数不一定能正常工作。如果在每个调用点缺省参数的值都有可能不同,在这种情况下缺省函数也不允许使用。(例如,不要写像void f(int n = counter++);这样的代码。)

在其他情况下,如果缺省参数对可读性的提升远远超过了以上提及的缺点的话,可以使用缺省参数。 如果仍有疑惑,就使用函数重载。

函数返回类型后置语法

总述

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

定义

C++现在允许两种不同的函数声明方式。以往的写法是将返回类型置于函数名之前。例如:

1
int foo(int x);

C++11引入了这一新的形式. 现在可以在函数名前使用auto关键字, 在参数列表之后后置返回类型. 例如:

1
auto foo(int x) -> int;

后置返回类型为函数作用域。对于像int这样简单的类型,两种写法没有区别。但对于复杂的情况,例如类域中的类型声明或者以函数参数的形式书写的类型,写法的不同会造成区别。

优点

后置返回类型是显式地指定Lambda表达式的返回值的唯一方式。某些情况下,编译器可以自动推导出Lambda表达式的返回类型,但并不是在所有的情况下都能实现。即使编译器能够自动推导,显式地指定返回类型也能让读者更明了。

有时在已经出现了的函数参数列表之后指定返回类型,能够让书写更简单,也更易读,尤其是在返回类型依赖于模板参数时。例如:

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

对比下面的例子:

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

缺点

后置返回类型相对来说是非常新的语法,而且在C和Java中都没有相似的写法,因此可能对读者来说比较陌生。

在已有的代码中有大量的函数声明,你不可能把它们都用新的语法重写一遍。因此实际的做法只能是使用旧的语法或者新旧混用。在这种情况下,只使用一种版本是相对来说更规整的形式。

结论

在大部分情况下,应当继续使用以往的函数声明写法,即将返回类型置于函数名前。只有在必需的时候(如Lambda表达式)或者使用后置语法能够简化书写并且提高易读性的时候才使用新的返回类型后置语法。但是后一种情况一般来说是很少见的,大部分时候都出现在相当复杂的模板代码中,而多数情况下不鼓励写这样复杂的模板代码

其他C++特性

命名约定

最重要的一致性规则是命名管理。命名的风格能让我们在不需要去查找类型声明的条件下快速地了解某个名字代表的含义:类型、变量、函数、常量、宏等等,甚至,我们大脑中的模式匹配引擎非常依赖这些命名规则。

命名规则具有一定随意性。但相比按个人喜好命名,一致性更重要,所以无论你认为它们是否重要,规则总是规则。

通用命名规则

总述

函数命名、变量命名,文件命名要有描述性;少用缩写。

说明

尽可能使用描述性的命名,别心疼空间,毕竟相比之下代码易于新读者理解更重要。不要用只有项目开发者能理解的缩写,也不要通过砍掉一个字母来缩写单词。

1
2
3
int price_count_reader;    // 无缩写
int num_errors; // "num" 是一个常见的写法
int num_dns_connections; // 人人都知道 "DNS" 是什么
1
2
3
4
5
6
int n;                     // 毫无意义.
int nerr; // 含糊不清的缩写.
int n_comp_conns; // 含糊不清的缩写.
int wgc_connections; // 只有贵团队知道是什么意思.
int pc_reader; // "pc" 有太多可能的解释了.
int cstmr_id; // 删减了若干字母.

注意,一些特定的广为人知的缩写是允许的,例如用i和用T表示模版参数。

模板参数的命名应当遵循对应的分类:类型模板参数应当遵循类型命名的规则,而非类型模板应当遵循变量命名的规则。

文件命名

总述

文件名要全部小写,可以包含下划线(_)和连字符(-),依照项目约定,如果没有约定,那么“_”更好。

说明

可接受的文件命名实例:

  • my_userful_class.cc
  • my-userful-class.cc
  • myuserfulclass.cc
  • myuserfulclass_test.cc // _unittest.cc_regtest.cc以弃用。

C++文件要以.cc结尾,头文件以.h结尾。专门插入文本的文件则以.inc 结尾,参见头文件自足

不要使用已经存在于/usr/include下的文件名。如:db.h

通常应尽量让文件名更加明确。http_servr.h就比logs.h要好。定义类时文件名一般成对出现,如:foo_bar.hfoo_bar.cc,对应于类
FooBar

内联函数必须放在.h文件中,如果内联函数比较短,就直接放在.h

类型命名

总述

类型名称的每个单词首字母均大写,不包好下划线:MyExcitingClassMyExcitingEnum

说明

所有类型命名——类、结构体、类型定义(typedef)、枚举、类型模板参数——均使用相同约定,即以大写字母开始,每个单词首字母均大写,不包含下划线。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 类和结构体
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// 类型定义
typedef hash_map<UrlTableProperties *, string> PropertiesMap;

// using 别名
using PropertiesMap = hash_map<UrlTableProperties *, string>;

// 枚举
enum UrlTableErrors { ...

变量命名

总述

变量(包含函数参数)和数据成员名一律小写,单词之间用下划线连接。类的成员变量以下划线结尾,但结构体就不用。比如:a_local_variablea_struct_data_membera_class_data_member

说明

普通变量命名
举例:

1
2
3
4
string table_name;  // 好 - 用下划线.
string tablename; // 好 - 全小写.

string tableName; // 差 - 混合大小写

类数据成员
不管是静态的还是非静态的,类数据成员都可以和普通变量一样,但要接下划线。

1
2
3
4
5
6
7
class TableInfo {
...
private:
string table_name_; // 好 - 后加下划线.
string tablename_; // 好.
static Pool<TableInfo>* pool_; // 好.
};

结构体变量
不管是静态的还是非静态的,结构体数据成员都可以和普通变量一样,不要像类那样接下划线。

1
2
3
4
5
struct UrlTableProperties {
string name;
int num_entries;
static Pool<UrlTableProperties>* pool;
};

结构体与类的使用讨论,参考结构体和类

常量命名

总述

声明为constexprconst的变量,或在程序运行期间其值始终保持不变的,命名时以“K”开头,大小写混合。例如:

1
const int kDaysInAWeek = 7;

说明

所有具有静态存储类型的变量(例如静态变量或全局变量,参见存储类型) 都应当以此方式命名。对于其他存储类型的变量,如自动变量等,这条规则是可选的。如果不采用这条规则,就按照一般的变量命名规则。

函数命名

总述

常规函数使用大小写混合,取值和设值函数则要求与变量名匹配:MyExcitingFunction()MyExcitingMethod()my_exciting_member_variable()set_my_exciting_member_variable()

说明

一般来说,函数名的每个单词首字母大写(即“驼峰变量名”或“帕斯卡变量名”),没有下划线。对于首字母缩写的单词,更倾向于将它们视作一个单词进行首字母大写(例如,写作StartRpc()而非StartRPC())。

1
2
3
AddTableEntry()
DeleteUrl()
OpenFileOrDie()

(同样的命名规则同时适用于类作用域与命名空间作用域的常量,因为它们是作为API的一部分暴露对外的,因此应当让它们看起来像是一个函数,因为在这时,它们实际上是一个对象而非函数的这一事实对外不过是一个无关紧要的实现细节。)

取值和设值函数的命名与变量一致。一般来说它们的名称与实际的成员变量对应,但并不强制要求.。例如sint count()void set_count(int count)

命名空间命名

总述

命名空间以小写字母命名。最高级命名空间的名字取决于项目名称。要注意避免嵌套命名空间的名字之间和行间的顶级命名空间的名字之间发生的冲突。

顶级命名空间的名称应当是项目名称或是该命名空间中的代码所属的团队的名字。命名空间中的代码,应该存放于和命名空间的名字匹配的文件夹或其子文件夹中。

注意不使用缩写作为名称的规则同样适用于命名空间。命名空间中的代码极少需要设计命名空间的名称,因为没有必要在命名空间中使用缩写。

要避免嵌套的命名空间与常见的顶级命名空间发生名称冲突。由于名称查找规则的存在,命名空间之间的冲突完全有可能导致编译失败。尤其是,不要创建嵌套的stdm命名空间。建议使用更独特的项目标识符(websearch::indexwebsearch::index_util),而非常见的极易发生冲突的名名称(比如:websearch::util)。

对于internal命名空间,要当心加入到同一internal命名空间的代码之间发生冲突(由于内部维护人员通常来自于同一个团队,因此常有可能导致冲突)。在这样的情况下,请使用文件名以使得内部名称独一无二(例如对于frobber.h,使用websearch::index::frobber_internal)。

枚举命名

总述

枚举的命名应当和常量一致:kEnumName或是ENUM_NAME

说明

单独的枚举值应该优先采用常量的命名方式。但方式的命名也可以接受,枚举名UrlTableErrors(以及AlternateUrlTableErrors)是类型,所以要用大小写混合的方式。

1
2
3
4
5
6
7
8
9
10
enum UrlTableErrors {
kOK = 0,
kErrorOutOfMemory,
kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
OK = 0,
OUT_OF_MEMORY = 1,
MALFORMED_INPUT = 2,
};

2009年1月之前,我们一直建议采用的方式命名枚举值。由于枚举值和宏之间的命名冲突,直接导致了很多问题。由此,这里改为优先选择常量风格的命名方式。新代码应该尽可能优先使用常量风格。但是老代码没必要切换到常量风格,除非宏风格确实会产生编译期问题。

宏命名

总述

你并不打算[使用宏],对吧?如果你一定要用,就像这样命名:MY_MACRO_THAT_SCARES_SMALL_CHILDREN

说明

参考预处理宏;通常,不应该使用宏,如果不得不用,其命名像枚举命名一样全部大写,使用下划线:

1
2
#define ROUND(x) ...
#define PI_ROUNDED 3.0

命名规则特例

总述

如果你命名的实体与已有C/C++实体相似,可参考现有命名策略。

bigopen():函数名,参照open()的形式。
uinttypedef
bigposstructclass,参照pos的形式。
sparse_hash_map:STL型实体;参照STL命名约定。
UrlTableErrors常量,如同INT_MAX

注释

注释虽然写起来很痛苦,但对保证代码可读性至关重要。下面的规则描述了如何注释以及在哪儿注释。当然也要记住:注释固然很重要,但最好的代码应当本身就是文档。有意义的类型名和变量名,要远胜过要用注释解释的含糊不清的名字。

你写的注释是给代码读者看的,也就是下一个需要理解你的代码的人。所以慷慨些吧,下一个读者可能就是你!

注释风格

总述

使用///* */,统一就好。

说明

虽然///* */都可以,但//更常用。更在如何注释以及注释风格上确保统一。

文件注释

总述

在每一个文件开头加入版权公告。

文件注释描述了该文件的内容。如果一个文件只声明,或实现,或测试了一个对象,并且这个对象已经在它的声明处进行了详细的注释,那么就没必要再加上文件注释。除此之外的其他文件都需要文件注释。

说明
法律公告和作者信息

每个文件都应该包含许可证引用。为项目选择合适的许可证版本。(比如,Apache 2.0、BSD、LGPL,、GPL)。

如果你对原始作者的文件做了重大修改。请考虑删除原作者信息。

文件内容

如果一个.h文件声明了多个概念,则文件注释应当对文件的内容做一个大致的说明,同时说明各概念之间的联系。一个一到两行的文件注释就足够了,对于每个概念的详细文档应当放在各个概念中,而不是文件注释中。

不要在.h.cc之间复制注释,这样的注释偏离了注释的实际意义.

类注释

总述

每个类的定义都要附带一份注释,描述类的功能和用法,除非它的功能相当明显。

1
2
3
4
5
6
7
8
9
10
// Iterates over the contents of a GargantuanTable.
// Example:
// GargantuanTableIterator* iter = table->NewIterator();
// for (iter->Seek("foo"); !iter->done(); iter->Next()) {
// process(iter->key(), iter->value());
// }
// delete iter;
class GargantuanTableIterator {
...
};

说明

类注释应当为读者理解如何使用与何时使用类提供足够的信息,同时应当提醒读者在正确使用此类时应当考虑的因素。如果类有任何同步前提,请用文档说明。如果该类的实例可被多线程访问,要特别注意文档说明多线程环境下相关的规则和常量使用。

如果你想用一小段代码演示这个类的基本用法或通常用法,放在类注释里也非常合适。

如果类的声明和定义分开了(例如分别放在了.h.cc文件中),此时,描述类用法的注释应当和接口定义放在一起,描述类的操作和实现的注释应当和实现放在一起。

函数注释

总述

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

说明
函数声明

基本上每个函数声明处前都应当加上注释,描述函数的功能和用途。只有在函数的功能简单而明显时才能省略这些注释(例如,简单的取值和设值函数)。注释使用叙述式(“Opens the file”)而非指令式 (“Open the file”);注释只是为了描述函数,而不是命令函数做什么。通常,注释不会描述函数如何工作。那是函数定义部分的事情。

函数声明处注释的内容:

  • 函数的输入输出。
  • 对类成员函数而言:函数调用期间对象是否需要保持引用参数,是否会释放这些参数。
  • 函数是否分配了必须由调用者释放的空间。
  • 参数是否可以为空指针。
  • 是否存在函数使用上的性能隐患。
  • 如果函数是可重入的,其同步前提是什么?

举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Returns an iterator for this table.  It is the client's
// responsibility to delete the iterator when it is done with it,
// and it must not use the iterator once the GargantuanTable object
// on which the iterator was created has been deleted.
//
// The iterator is initially positioned at the beginning of the table.
//
// This method is equivalent to:
// Iterator* iter = table->NewIterator();
// iter->Seek("");
// return iter;
// If you are going to immediately seek to another place in the
// returned iterator, it will be faster to use NewIterator()
// and avoid the extra seek.
Iterator* GetIterator() const;

但也要避免罗罗嗦嗦,或者对显而易见的内容进行说明。下面的注释就没有必要加上“否则返回 false”, 因为已经暗含其中了:

1
2
// Returns true if the table cannot hold any more entries.
bool IsTableFull();

注释函数重载时,注释的重点应该是函数中被重载的部分,而不是简单的重复被重载的函数的注释。多数情况下,函数重载不需要额外的文档,因此也没有必要加上注释。

注释构造/析构函数时,切记读代码的人知道构造/析构函数的功能,所以“销毁这一对象”这样的注释是没有意义的。你应当注明的是注明构造函数对参数做了什么(例如,是否取得指针所有权)以及析构函数清理了什么。如果都是些无关紧要的内容,直接省掉注释。析构函数前没有注释是很正常的。

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

变量注释

总述

通常变量名本身足以很好说明变量用途。某些情况下,也需要额外的注释说明。

说明
类数据成员

每个类数据成员(也叫实例变量或成员变量)都应该用注释说明用途。如果有非变量的参数(例如特殊值,数据成员之间的关系、生命周期等)不能够用类型与变量名明确表达,则应当加上注释。然而,如果变量类型与变量名已经足以描述一个变量,那么就不再需要加上注释。

特别地,如果变量可以接受NULL-1等警戒值,须加以说明。比如:

1
2
3
4
private:
// Used to bounds-check table accesses. -1 means
// that we don't yet know how many entries the table has.
int num_total_entries_;
全局变量

和数据成员一样,所有全局变量也要注释说明含义及用途,以及作为全局变量的原因。比如:

1
2
// The total number of tests cases that we run through in this regression test.
const int kNumTestCases = 6;

实现注释

总述

对于代码中巧妙的,晦涩的,有趣的,重要的地方加以注释。

说明
代码前注释

巧妙或复杂的代码段前要加注释。比如:

1
2
3
4
5
6
7
// Divide result by two, taking into account that x
// contains the carry from the add.
for (int i = 0; i < result->size(); i++) {
x = (x << 8) + (*result)[i];
(*result)[i] = x >> 1;
x &= 1;
}

行注释

比较隐晦的地方要在行尾加入注释。在行尾空两格进行注释。比如:

1
2
3
4
// If we have enough memory, mmap the data portion too.
mmap_budget = max<int64>(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock))
return; // Error already logged.

注意,这里用了两段注释分别描述这段代码的作用,和提示函数返回时错误已经被记入日志。

如果你需要连续进行多行注释,可以使之对齐获得更好的可读性:

1
2
3
4
5
6
7
8
9
10
11
12
DoSomething();                  // Comment here so the comments line up.
DoSomethingElseThatIsLonger(); // Two spaces between the code and the comment.
{ // One space before comment when opening a new scope is allowed,
// thus the comment lines up with the following comments and code.
DoSomethingElse(); // Two spaces before line comments normally.
}
std::vector<string> list{
// Comments in braced lists describe the next element...
"First item",
// .. and should be aligned appropriately.
"Second item"};
DoSomething(); /* For trailing block comments, one space is fine. */

函数参数注释

如果函数参数的意义不明显,考虑用下面的方式进行弥补:

  • 如果参数是一个字面常量,并且这一常量在多处函数调用中被使用,用以推断它们一致,你应当用一个常量名让这一约定变得更明显,并且保证这一约定不会被打破。
  • 考虑更改函数的签名,让某个bool类型的参数变为enum类型,这样可以让这个参数的值表达其意义。
  • 如果某个函数有多个配置选项,你可以考虑定义一个类或结构体以保存所有的选项,并传入类或结构体的实例。这样的方法有许多优点,例如这样的选项可以在调用处用变量名引用,这样就能清晰地表明其意义。同时也减少了函数参数的数量,使得函数调用更易读也易写。除此之外,以这样的方式,如果你使用其他的选项,就无需对调用点进行更改。
  • 用具名变量代替大段而复杂的嵌套表达式。
  • 万不得已时, 才考虑在调用点用注释阐明参数的意义.

比如下面的示例的对比:

1
2
// What are these arguments?
const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);


1
2
3
4
5
ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product =
CalculateProduct(values, options, /*completion_callback=*/nullptr);

哪个更清晰一目了然。

不允许的行为

不要描述显而易见的现象,永远不要用自然语言翻译代码作为注释,除非即使对深入理解C++的读者来说代码的行为都是不明显的。要假设读代码的人C++水平比你高,即便他/她可能不知道你的用意:

你所提供的注释应当解释代码为什么要这么做和代码的目的,或者最好是让代码自文档化。

比较这样的注释:

1
2
3
4
5
// Find the element in the vector.  <-- 差: 这太明显了!
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
Process(element);
}

和这样的注释:

1
2
3
4
5
// Process "element" unless it was already processed.
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
Process(element);
}

自文档化的代码根本就不需要注释。上面例子中的注释对下面的代码来说就是毫无必要的:

1
2
3
if (!IsAlreadyProcessed(element)) {
Process(element);
}

标点、拼写和语法

总述

注意标点,拼写和语法;写的好的注释比差的要易读的多。

说明

注释的通常写法是包含正确大小写和结尾句号的完整叙述性语句。大多数情况下,完整的句子比句子片段可读性更高。短一点的注释,比如代码行尾注释,可以随意点。但依然要注意风格的一致性.

虽然被别人指出该用分号时却用了逗号多少有些尴尬,但清晰易读的代码还是很重要的。正确的标点、拼写和语法对此会有很大帮助。

TODO注释

总述

对那些临时的、短期的解决方案,或已经够好但仍不完美的代码使用TODO注释。

说明

TODO注释要使用全大写的字符串TODO,在随后的圆括号里写上你的名字、邮件地址、bug ID或其它身份标识和与这一TODO相关的issue。主要目的是让添加注释的人(也是可以请求提供更多细节的人)可根据规范的TODO格式进行查找。添加TODO注释并不意味着你要自己来修正,因此当你加上带有姓名的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

如果加TODO为了在“将来某一天做某事”,可以附上一个非常明确的时间 “Fix by November 2005”),或者一个明确的事项(“Remove this code when all clients can handle XML responses.”)。

弃用注释

总述

通过弃用注释(DEPRECATED comments)以标记某接口点已弃用。

您可以写上包含全大写的DEPRECATED的注释,以标记某接口为弃用状态。 注释可以放在接口声明前或者同一行。

DEPRECATED一词后,在括号中留下您的名字,邮箱地址以及其他身份标识。

弃用注释应当包涵简短而清晰的指引,以帮助其他人修复其调用点。在 C++ 中,你可以将一个弃用函数改造成一个内联函数,这一函数将调用新的接口。

仅仅标记接口为DEPRECATED并不会让大家不约而同地弃用,您还得亲自主动修正调用点(callsites)或是找个帮手。

修正好的代码应该不会再涉及弃用接口点了,着实改用新接口点。如果您不知从何下手,可以找标记弃用注释的当事人一起商量。

格式

每个人都可能有自己的代码风格和格式,但如果一个项目中的所有人都遵循同一风格的话,这个项目就能更顺利地进行。每个人未必能同意下述的每一处格式规则,而且其中的不少规则需要一定时间的适应,但整个项目服从统一的编程风格是很重要的,只有这样才能让所有人轻松地阅读和理解代码。

为了帮助你正确的格式化代码,我们写了一个emacs配置文件

行长度

总述

每一行代码字符数不超过80。

我们也认识到这条规则是有争议的,但很多已有代码都遵照这一规则,因此我们感觉一致性更重要。

优点

提倡该原则的人认为强迫他们调整编辑器窗口大小是很野蛮的行为。很多人同时并排开几个代码窗口,根本没有多余的空间拉伸窗口。大家都把窗口最大尺寸加以限定并且80列宽是传统标准。那么为什么要改变呢?

缺点

反对该原则的人则认为更宽的代码行更易阅读。80列的限制是上个世纪60年代的大型机的古板缺陷;现代设备具有更宽的显示屏,可以很轻松地显示更多代码。

结论

如果无法在不伤害易读性的条件下进行断行,那么注释行可以超过80个字符,这样可以方便复制粘贴。例如,带有命令示例或URL的行可以超过80个字符。

包含长路径的#include语句可以超出80列。

头文件保护可以无视该原则.

非ASCII字符

总述

尽量不使用非ASCII字符,使用时必须使用UTF-8编码。

说明

即使是英文,也不应将用户界面的文本硬编码到源代码中,因此非ASCII字符应当很少被用到。特殊情况下可以适当包含此类字符。例如,代码分析外部数据文件时,可以适当硬编码数据文件中作为分隔符的非 ASCII字符串;更常见的是(不需要本地化的)单元测试代码可能包含非ASCII字符串。此类情况下,应使用UTF-8编码,因为很多工具都可以理解和处理UTF-8编码。

十六进制编码也可以,能增强可读性的情况下尤其鼓励 —— 比如"\xEF\xBB\xBF",或者更简洁地写作u8"\uFEFF",在Unicode中是零宽度 无间断的间隔符号,如果不用十六进制直接放在UTF-8格式的源文件中,是看不到的。

使用u8前缀把带uXXXX转义序列的字符串字面值编码成UTF-8。不要用在本身就带UTF-8字符的字符串字面值上,因为如果编译器不把源代码识别成UTF-8,输出就会出错。

别用C++11的char16_tchar32_t,它们和UTF-8文本没有关系,wchar_t同理,除非你写的代码要调用Windows API,后者广泛使用了wchar_t

空格还是制表符

总述

只使用空格,每次缩进2个空格。

说明

我们使用空格缩,不要在代码中使用制表符。你应该设置编辑器将制表符转为空格。

函数声明和定义

总述

返回类型和函数名在同一行,参数也尽量放在同一行,如果放不下就对形参分行,分行方式与函数调用一致。

说明

函数看上去像这样:

1
2
3
4
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
DoSomething();
...
}

如果同一行文本太多,放不下所有参数:

1
2
3
4
5
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
Type par_name3) {
DoSomething();
...
}

甚至连第一个参数都放不下:

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

注意以下几点:

  • 使用好的参数名。
  • 只有在参数违背使用或者其用途非常明显时,才能省略参数名。
  • 如果返回类型和函数名在一行放不下,分行。
  • 如果返回类型与函数声明或定义分行了,不要缩进。
  • 左圆括号总是和函数名在同一行。
  • 函数名和左圆括号间永远没有空格。
  • 圆括号与参数间没有空格。
  • 左大括号总在最后一个参数同一行的末尾处,不另起新行。
  • 右大括号总是单独位于函数最后一行,或者与左大括号同一行。
  • 右圆括号和左大括号间总是有一个空格。
  • 所有形参应尽可能对齐。
  • 缺省缩进为2个空格。
  • 换行后的参数保持4个空格的缩进。

未被使用的参数,或者根据上下文很容易看出其用途的参数,可以省略参数名:

1
2
3
4
5
6
7
class Foo {
public:
Foo(Foo&&);
Foo(const Foo&);
Foo& operator=(Foo&&);
Foo& operator=(const Foo&);
};

未被使用的参数如果其用途不明显的话,在函数定义处将参数名注释起来:

1
2
3
4
5
6
7
8
9
10
11
class Shape {
public:
virtual void Rotate(double radians) = 0;
};

class Circle : public Shape {
public:
void Rotate(double radians) override;
};

void Circle::Rotate(double /*radians*/) {}

1
2
/ 差 - 如果将来有人要实现, 很难猜出变量的作用.
void Circle::Rotate(double) {}

属性和展开为属性的宏,写在函数声明或定义的最前面,即返回类型之前:

1
MUST_USE_RESULT bool IsOK();

Lambda表达式

总述

Lambda表达式对形参和函数体的格式化和其他函数一致;捕获列表同理,表项用逗号隔开。

说明

若用引用捕获,在变量名和&之间不留空格。

1
2
int x = 0;
auto add_to_x = [&x](int n) { x += n; };

短lambda就写得和内联函数一样。

1
2
3
4
5
6
std::set<int> blacklist = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {
return blacklist.find(i) != blacklist.end();
}),
digits.end());

函数调用

总述

要么一行写完函数调用,要么在圆括号里对参数分行,要么参数另起一行且缩进四格。如果没有其它顾虑的话,尽可能精简行数,比如把多个参数适当地放在同一行里。

说明

函数调用遵循如下形式:

1
bool retval = DoSomething(argument1, argument2, argument3);

如果同一行放不下,可断为多行,后面每一行都和第一个实参对齐,左圆括号后和右圆括号前不要留空格:

1
2
bool retval = DoSomething(averyveryveryverylongargument1,
argument2, argument3);

参数也可以放在次行,缩进四格:

1
2
3
4
5
6
7
8
if (...) {
...
...
if (...) {
DoSomething(
argument1, argument2, // 4 空格缩进
argument3, argument4);
}

把多个参数放在同一行以减少函数调用所需的行数,除非影响到可读性。有人认为把每个参数都独立成行,不仅更好读而且方便编辑参数。不过,比起所谓的参数编辑,我们更看重可读性,且后者比较好办。

如果一些参数本身就是略复杂的表达式,且降低了可读性,那么可以直接创建临时变量描述该表达式,并传递给函数:

1
2
int my_heuristic = scores[x] * y + bases[x];
bool retval = DoSomething(my_heuristic, x, y, z);

或者放着不管,补充上注释:

1
2
bool retval = DoSomething(scores[x] * y + bases[x],  // Score heuristic.
x, y, z);

如果某参数独立成行,对可读性更有帮助的话,那也可以如此做。参数的格式处理应当以可读性而非其他作为最重要的原则。

此外,如果一系列参数本身就有一定的结构,可以酌情地按其结构来决定参数格式:

1
2
3
4
// 通过 3x3 矩阵转换 widget.
my_widget.Transform(x1, x2, x3,
y1, y2, y3,
z1, z2, z3);

列表初始化格式

总述

您平时怎么格式化函数调用,就怎么格式化列表初始化

说明

如果列表初始化伴随着名字,比如类型或变量名,格式化时将将名字视作函数调用名,{}视作函数调用的括号。如果没有名字,就视作名字长度为零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 一行列表初始化示范.
return {foo, bar};
functioncall({foo, bar});
pair<int, int> p{foo, bar};

// 当不得不断行时.
SomeFunction(
{"assume a zero-length name before {"}, // 假设在 { 前有长度为零的名字.
some_other_function_parameter);
SomeType variable{
some, other, values,
{"assume a zero-length name before {"}, // 假设在 { 前有长度为零的名字.
SomeOtherType{
"Very long string requiring the surrounding breaks.", // 非常长的字符串, 前后都需要断行.
some, other values},
SomeOtherType{"Slightly shorter string", // 稍短的字符串.
some, other, values}};
SomeType variable{
"This is too long to fit all in one line"}; // 字符串过长, 因此无法放在同一行.
MyType m = { // 注意了, 您可以在 { 前断行.
superlongvariablename1,
superlongvariablename2,
{short, interior, list},
{interiorwrappinglist,
interiorwrappinglist2}};

条件语句

总述

倾向于不在圆括号内使用空格。关键字ifelse另起一行。

说明

对基本条件语句有两种可以接受的格式。一种在圆括号和条件之间有空格,另一种没有。

最常见的是没有空格的格式,哪一种都可以,最重要的是保持一致,如果你是在修改一个文件,参考当前已有格式。如果是写新的代码,参考目录下或项目中其它文件。还在犹豫的话,就不要加空格了。

1
2
3
4
5
6
7
if (condition) {  // 圆括号里没有空格.
... // 2 空格缩进.
} else if (...) { // else 与 if 的右括号同一行.
...
} else {
...
}

如果你更喜欢在圆括号内部加空格:

1
2
3
4
5
if ( condition ) {  // 圆括号与空格紧邻 - 不常见
... // 2 空格缩进.
} else { // else 与 if 的右括号同一行.
...
}

注意所有情况下if和左圆括号间都有个空格。右圆括号和左大括号之间也要有个空格:

1
2
3
if(condition)     // 差 - IF 后面没空格.
if (condition){ // 差 - { 前面没空格.
if(condition){ // 变本加厉地差.

1
if (condition) {  // 好 - IF 和 { 都与空格紧邻.

如果能增强可读性,简短的条件语句允许写在同一行。只有当语句简单并且没有使用else子句时使用:

1
2
if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();

如果语句有else分支则不允许:

1
2
3
// 不允许 - 当有 ELSE 分支时 IF 块却写在同一行
if (x) DoThis();
else DoThat();

通常,单行语句不需要使用大括号,如果你喜欢用也没问题;复杂的条件或循环语句用大括号可读性会更好。也有一些项目要求if必须总是使用大括号:

1
2
3
4
5
6
if (condition)
DoSomething(); // 2 空格缩进.

if (condition) {
DoSomething(); // 2 空格缩进.
}

但如果语句中某个if-else分支使用了大括号的话,其它分支也必须使用:

1
2
3
4
5
6
7
8
9
10
11
12
// 不可以这样子 - IF 有大括号 ELSE 却没有.
if (condition) {
foo;
} else
bar;

// 不可以这样子 - ELSE 有大括号 IF 却没有.
if (condition)
foo;
else {
bar;
}

1
2
3
4
5
6
// 只要其中一个分支用了大括号, 两个分支都要用上大括号.
if (condition) {
foo;
} else {
bar;
}

循环和开关选择语句

总述

switch语句可以使用大括号分段,以表明font color=red>case之间不是连在一起的。在单语句循环里,括号可用可不用。空循环体应使用font color=red>{}或font color=red>switch

说明

switch语句中的case块可以使用大括号也可以不用,取决于你的个人喜好。如果用的话,要按照下文所述的方法。

如果有不满足case条件的枚举值,switch应该总是包含一个default匹配(如果有输入值没有case去处理,编译器将给出warning)。如果default应该永远执行不到,简单的加条assert:

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (var) {
case 0: { // 2 空格缩进
... // 4 空格缩进
break;
}
case 1: {
...
break;
}
default: {
assert(false);
}
}

在单语句循环里,括号可用可不用:

1
2
3
4
5
6
for (int i = 0; i < kSomeNumber; ++i)
printf("I love you\n");

for (int i = 0; i < kSomeNumber; ++i) {
printf("I take it back\n");
}

空循环体应使用{}continue,而不是一个简单的分号。

1
2
3
4
5
while (condition) {
// 反复循环直到条件失效.
}
for (int i = 0; i < kSomeNumber; ++i) {} // 可 - 空循环体.
while (condition) continue; // 可 - contunue 表明没有逻辑.

1
while (condition);  // 差 - 看起来仅仅只是 while/loop 的部分之一.

指针和引用表达式

总述

句点或箭头前后不要有空格。指针/地址操作符(*,&)之后不能有空格。

说明

下面是指针和引用表达式的正确使用范例:

1
2
3
4
x = *p;
p = &x;
x = r.y;
x = r->y;

注意:

  • 在访问成员时,句点或箭头前后没有空格。
  • 指针操作符*&后没有空格.

在声明指针变量或参数时,星号与类型或变量名紧挨都可以:

1
2
3
4
5
6
7
// 好, 空格前置.
char *c;
const string &str;

// 好, 空格后置.
char* c;
const string& str;

1
2
3
int x, *y;  // 不允许 - 在多重声明中不能使用 & 或 *
char * c; // 差 - * 两边都有空格
const string & str; // 差 - & 两边都有空格.

在单个文件内要保持风格一致,所以,如果是修改现有文件,要遵照该文件的风格。

布尔表达式

总述

如果一个布尔表达式超过标准行宽,断行方式要统一一下。

说明

下例中,逻辑与(&&)操作符总位于行尾:

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

注意,上例的逻辑与(&&)操作符均位于行尾。这个格式在Google里很常见,虽然把所有操作符放在开头也可以。可以考虑额外插入圆括号,合理使用的话对增强可读性是很有帮助的。此外,直接用符号形式的操作符,比如&&,不要用词语形式的andcompl

函数返回类型

总述

不要在return表达式里加上非必须的圆括号。

说明

只有在写x = expr要加上括号的时候才在return expr;里使用括号。

1
2
3
4
return result;                  // 返回值很简单, 没有圆括号.
// 可以用圆括号把复杂表达式圈起来, 改善可读性.
return (some_long_condition &&
another_condition);

1
2
return (value);                // 毕竟您从来不会写 var = (value);
return(result); // return 可不是函数!

变量及数组初始化

总述

=(){}均可。

说明

您可以用=(){},以下的例子都是正确的:

1
2
3
4
5
6
int x = 3;
int x(3);
int x{3};
string name("Some Name");
string name = "Some Name";
string name{"Some Name"};

请务必小心列表初始化{...}std::initializer_list构造函数初始化出的类型。非空列表初始化就会优先调用std::initializer_list,不过空列表初始化除外,后者原则上会调用默认构造函数。为了强制禁用std::initializer_list构造函数,请改用括号。

1
2
vector<int> v(100, 1);  // 内容为 100 个 1 的向量.
vector<int> v{100, 1}; // 内容为 100 和 1 的向量.

此外,列表初始化不允许整型类型的四舍五入,这可以用来避免一些类型上的编程失误。

1
2
int pi(3.14);  // 好 - pi == 3.
int pi{3.14}; // 编译错误: 缩窄转换.

预处理指令

总述

预处理指令不要缩进,从行首开始。

说明

即使预处理指令位于缩进代码块中,指令也应从行首开始。

1
2
3
4
5
6
7
8
9
10
// 好 - 指令从行首开始
if (lopsided_score) {
#if DISASTER_PENDING // 正确 - 从行首开始
DropEverything();
# if NOTIFY // 非必要 - # 后跟空格
NotifyClient();
# endif
#endif
BackToNormal();
}

1
2
3
4
5
6
7
// 差 - 指令缩进
if (lopsided_score) {
#if DISASTER_PENDING // 差 - "#if" 应该放在行开头
DropEverything();
#endif // 差 - ”#endif“不要缩进
BackToNormal();
}

类格式

总述

访问控制块的声明依次序是public:protected:private:,每个都缩进 1 个空格。

说明

类声明(下面的代码中缺少注释,参考类注释)的基本格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass : public OtherClass {
public: // 注意有一个空格的缩进
MyClass(); // 标准的两空格缩进
explicit MyClass(int var);
~MyClass() {}

void SomeFunction();
void SomeFunctionThatDoesNothing() {
}

void set_some_var(int var) { some_var_ = var; }
int some_var() const { return some_var_; }

private:
bool SomeInternalFunction();

int some_var_;
int some_other_var_;
};

注意事项:

  • 所有基类名应在80列限制下尽量与子类名放在同一行。
  • 关键词public:protected:private:要缩进1个空格。
  • 除第一个关键词(一般是public:)外,其他关键词前要空一行。如果类比较小的话也可以不空。
  • public:放在最前面,然后是protected:,最后是private:
  • 关于声明顺序的规则请参考声明顺序一节。

构造函数初始化列表

总述

构造函数初始化列表放在同一行或按四格缩进并排多行。

说明

下面两种初始值列表方式都可以接受:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 如果所有变量能放在同一行:
MyClass::MyClass(int var) : some_var_(var) {
DoSomething();
}

// 如果不能放在同一行,
// 必须置于冒号后, 并缩进 4 个空格
MyClass::MyClass(int var)
: some_var_(var), some_other_var_(var + 1) {
DoSomething();
}

// 如果初始化列表需要置于多行, 将每一个成员放在单独的一行
// 并逐行对齐
MyClass::MyClass(int var)
: some_var_(var), // 4 space indent
some_other_var_(var + 1) { // lined up
DoSomething();
}

// 右大括号 } 可以和左大括号 { 放在同一行
// 如果这样做合适的话
MyClass::MyClass(int var)
: some_var_(var) {}

命名空间格式化

总述

命名空间内容不缩进。

说明

命名空间不要增加额外的缩进层次,例如:

1
2
3
4
5
6
7
namespace {

void foo() { // 正确. 命名空间内没有额外的缩进.
...
}

} // namespace

不要在命名空间内缩进:

1
2
3
4
5
6
7
8
namespace {

// 错, 缩进多余了.
void foo() {
...
}

} // namespace

声明嵌套命名空间时,每个命名空间都独立成行。

1
2
namespace foo {
namespace bar {

水平留白

总述

水平留白的使用根据在代码中的位置决定,永远不要在行尾添加没意义的留白。

说明

通用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void f(bool b) {  // 左大括号前总是有空格.
...
int i = 0; // 分号前不加空格.
// 列表初始化中大括号内的空格是可选的.
// 如果加了空格, 那么两边都要加上.
int x[] = { 0 };
int x[] = {0};

// 继承与初始化列表中的冒号前后恒有空格.
class Foo : public Bar {
public:
// 对于单行函数的实现, 在大括号内加上空格
// 然后是函数实现
Foo(int b) : Bar(), baz_(b) {} // 大括号里面是空的话, 不加空格.
void Reset() { baz_ = 0; } // 用括号把大括号与实现分开.
...

添加冗余的留白会给其他人编辑时造成额外负担。因此,行尾不要留空格。如果确定一行代码已经修改完毕,将多余的空格去掉;或者在专门清理空格时去掉(尤其是在没有其他人在处理这件事的时候)。

循环和条件语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (b) {          // if 条件语句和循环语句关键字后均有空格.
} else { // else 前后有空格.
}
while (test) {} // 圆括号内部不紧邻空格.
switch (i) {
for (int i = 0; i < 5; ++i) {
switch ( i ) { // 循环和条件语句的圆括号里可以与空格紧邻.
if ( test ) { // 圆括号, 但这很少见. 总之要一致.
for ( int i = 0; i < 5; ++i ) {
for ( ; i < 5 ; ++i) { // 循环里内 ; 后恒有空格, ; 前可以加个空格.
switch (i) {
case 1: // switch case 的冒号前无空格.
...
case 2: break; // 如果冒号有代码, 加个空格.

操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
// 赋值运算符前后总是有空格.
x = 0;

// 其它二元操作符也前后恒有空格, 不过对于表达式的子式可以不加空格.
// 圆括号内部没有紧邻空格.
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);

// 在参数和一元操作符之间不加空格.
x = -5;
++x;
if (x && !y)

模板和转换

1
2
3
4
5
6
// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有.
vector<string> x;
y = static_cast<char*>(x);

// 在类型与指针操作符之间留空格也可以, 但要保持一致.
vector<char *> x;

垂直留白

总述

垂直留白越少越好.

说明

这不仅仅是规则而是原则问题了:不在万不得已,不要使用空行。尤其是: 两个函数定义之间的空行不要超过2行,函数体首尾不要留空行,函数体中也不要随意添加空行。

基本原则是: 同一屏可以显示的代码越多,越容易理解程序的控制流。当然,过于密集的代码块和过于疏松的代码块同样难看,这取决于你的判断。但通常是垂直留白越少越好。

下面的规则可以让加入的空行更有效:

  • 函数体内开头或结尾的空行可读性微乎其微。
  • 在多重if-else块里加空行或许有点可读性。

规则特例

前面说明的编程习惯基本都是强制性的。但所有优秀的规则都允许例外,这里就是探讨这些特例。

现有不合规范代码

总述

对于现有不符合既定编程风格的代码可以网开一面。

说明

当你修改使用其他风格的代码时,为了与代码原有风格保持一致可以不使用本指南约定。如果不放心, 可以与代码原作者或现在的负责人员商讨。记住,一致性也包括原有的一致性。

Windows代码

总述

Windows程序员有自己的编程习惯,主要源于Windows头文件和其它Microsoft代码。我们希望任何人都可以顺利读懂你的代码,所以针对所有平台的C++编程只给出一个单独的指南。

说明

如果你习惯使用Windows编码风格,这儿有必要重申一下某些你可能会忘记的指南:

  • 不要使用匈牙利命名法(比如把整型变量命名成iNum)。使用 Google命名约定,包括对源文件使用.cc扩展名。
  • Windows定义了很多原生类型的同义词,如DWORDHANDLE等等。在调用Windows API时这是完全可以接受甚至鼓励的。即使如此,还是尽量使用原有的C++类型,例如使用const TCHAR *而不是 LPCSTR
  • 使用Microsoft Visual C++进行编译时,将警告级别设置为3或更高,并将所有警告(warnings)当作错误(errors)处理.
  • 不要使用#pragma once;而应该使用Google的头文件保护规则。 头文件保护的路径应该相对于项目根目录。
  • 除非万不得已,不要使用任何非标准的扩展,如#program__declspec。使用__declspec(dllimport)__declspec(dllxeport)是允许的,但必须通过宏来使用,比如 DLLIMPORTDLLEXPORT ,这样其他人在分享使用这些代码时可以很容易地禁用这些扩展。

然而,在Windows上仍然有一些我们偶尔需要违反的规则:

  • 通常我们禁止使用多重继承,但在使用COM和ATL/WTL类时可以使用多重继承。为了实现COM或 ATL/WTL类/接口,你可能不得不使用多重实现继承。
  • 虽然代码中不应该使用异常,但是在ATL和部分STL(包括Visual C++的STL)中异常被广泛使用。使用ATL时,应定义_ATL_NO_EXCEPTIONS 以禁用异常。你需要研究一下是否能够禁用STL的异常,如果无法禁用,可以启用编译器异常。(注意这只是为了编译STL,自己的代码里仍然不应当包含异常处理)。
  • 通常为了利用头文件预编译,每个每个源文件的开头都会包含一个名为StdAfx.hprecompile.h的文件。为了使代码方便与其他项目共享,请避免显式包含此文件(除了在precompile.cc中),使用/FI编译器选项以自动包含该文件.
  • 资源头文件通常命名为resource.h且只包含宏,这一文件不需要遵守本风格指南。

来自Google的奇特(Cpplint)

Google用了很多自己实现的技巧/工具使C++代码更加健壮,我们使用C++的方式可能和你在其它地方见到的有所不同。

所有权与智能指针

> 总述

动态分配出的对象最好有单一且固定的所有主,并通过智能指针传递所有权。

> 定义

所有权是一种登记/管理动态内存和其它资源的技术。动态分配对象的所有主是一个对象或函数,后者负责确保当前者无用时就自动销毁前者。所有权有时可以共享,此时就由最后一个所有主来负责销毁它。甚至也可以不用共享,在代码中直接把所有权传递给其它对象。

智能指针是一个通过重载*->运算符以表现得如指针一样的类。智能指针类型被用来自动化所有权的登记工作,来确保执行销毁义务到位。std::unique_ptr是C++11新推出的一种智能指针类型,用来表示动态分配出的对象的独一无二的所有权;当std::unique_ptr](http://en.cppreference.com/w/cpp/memory/unique_ptr)离开作用域时,对象就会被销毁。std::unique_ptr不能被复制,但可以把它移动(move)给新所有主。std::unique_ptr同样表示动态分配对象的所有权,但可以被共享,也可以被复制;对象的所有权由所有复制者共同拥有,最后一个复制者被销毁时,对象也会随着被销毁。

> 优点
  • 如果没有清晰、逻辑条理的所有权安排,不可能管理好动态分配的内存。
  • 传递对象的所有权,开销比复制来得小,如果可以复制的话。
  • 传递所有权也比”借用”指针或引用来得简单,毕竟它大大省去了两个用户一起协调对象生命周期的工作。
  • 如果所有权逻辑条理,有文档且不紊乱的话,可读性会有很大提升。
  • 可以不用手动完成所有权的登记工作,大大简化了代码,也免去了一大波错误之恼。
  • 对于const对象来说,智能指针简单易用,也比深度复制高效。
> 缺点
  • 不得不用指针(不管是智能的还是原生的)来表示和传递所有权。指针语义可要比值语义复杂得许多了,特别是在API里:这时不光要操心所有权,还要顾及别名、生命周期、可变性以及其它大大小小的问题。
  • 其实值语义的开销经常被高估,所以所有权传递带来的性能提升不一定能弥补可读性和复杂度的损失。
  • 如果API依赖所有权的传递,就会害得客户端不得不用单一的内存管理模型。
  • 如果使用智能指针,那么资源释放发生的位置就会变得不那么明显。
  • std::unique_ptr的所有权传递原理是C++11的move语法,后者毕竟是刚刚推出的,容易迷惑程序员。
  • 如果原本的所有权设计已经够完善了,那么若要引入所有权共享机制,可能不得不重构整个系统。
  • 所有权共享机制的登记工作在运行时进行,开销可能相当大。
  • 某些极端情况下 (例如循环引用),所有权被共享的对象永远不会被销毁。
  • 智能指针并不能够完全代替原生指针。
> 结论

如果必须使用动态分配,那么更倾向于将所有权保持在分配者手中。如果其他地方要使用这个对象,最好传递它的拷贝,或者传递一个不用改变所有权的指针或引用。倾向于使用std::unique_ptr来明确所有权传递,例如:

1
2
std::unique_ptr<Foo> FooFactory();
void FooConsumer(std::unique_ptr<Foo> ptr);

如果没有很好的理由,则不要使用共享所有权。这里的理由可以是为了避免开销昂贵的拷贝操作,但是只有当性能提升非常明显,并且操作的对象是不可变的(比如说std::shared_ptr<const Foo>)时候,才能这么做。如果确实要使用共享所有权, 建议于使用std::shared_ptr.

不要使用std::auto_ptr,使用std::unique_ptr代替它.

Cpplint

> 总述

使用cpplint.py检查风格错误。

> 说明

cpplint.py是一个用来分析源文件,能检查出多种风格错误的工具,它不并完美,甚至还会漏报和误报,但它仍然是一个非常有用的工具。在行尾加//NOLINT或在上一行加// NOLINTNEXTLINE,可以忽略报错。

某些项目会指导你如何使用他们的项目工具运行 cpplint.py。如果你参与的项目没有提供,你可以单独下载cpplint.py