Google开发规范

v0.2 - Last updated November 8, 2013

源自 Google's C++ coding style rev. 3.274

目录 由 DocToc生成
     头文件
        #define用法
        前向声明
        内联函数
        -inl.h文件
        函数参数顺序
        include的命名和顺序
    作用域
        命名空间
            未命名空间
            命名空间
        嵌套类
        非成员函数、静态成员函数、全局函数
        局部变量
        静态变量和全局变量
    类
        在构造函数里面完成工作
        初始化
        显式构造函数
        拷贝构造函数
        委派和继承构造函数
        结构体 vs 类
        继承
        多重继承
        接口
        操作符重载
        访问控制
        声明顺序
        编写短函数
    其它C++特性
        所有权和智能指针
        引用参数
        右值引用
        函数重载
        缺省参数
        变长数组和alloca
        友元
        异常
        运行时类型识别
        转换
        流
        前置自增和自减
        const用法
        constexpr用法
        Integer类型
            Unsigned Integers类型
        64位移植性
        预处理宏
        0和nullptr/NULL
        sizeof
        auto
        大括号初始化
        Lambda表达式
        Boost
        C++11
        一般命名规则
        文件名
        类型名
        变量名
            普通变量名
            类数据成员
            结构体变量
            全局变量
        常量名
        函数名称
            一般函数
            访问器和存储器
        命名空间的名称
        枚举器名称
        宏命名
        异常的命名规则
            bigopen()
            uint
            bigpos
            sparse_hash_map
            LONGLONG_MAX
    注释
        Doxygen
        注释规范
        文件注释
            法律声明和作者
            文件内容
        类注释
        函数注释
            函数声明
            函数定义
        变量注释
            类成员
            全局变量
        实现注释
            类数据成员
            单行注释
            nullptr/NULL, true/false, 1, 2, 3...
            Don'ts
        标点,拼写和语法
        TODO注释
        弃用注释
    格式化
        行长度
        非ASCII字符
        空格还是制表位
        函数声明与定义
        函数调用
        大括号初始化列表
        条件语句
        循环和选择语句
        指针和引用表达式
        布尔表达式
        返回值
        变量和数组初始化
        预处理器指令
        类格式
        构造函数初始化列表
        命名空间格式化
        水平空白
            一般
            循环和条件
            操作符
            模版和类型转换
        垂直空白
    例外的规则
        现存的不符合标准的代码
        Windows代码

头文件

一般情况下,每.CPP文件应该有一个相关的·h文件。有一些常见的例外,如单元测试代码和只包含一个main函数的cpp文件。

正确使用头文件在可读性,文件大小和性能上有很大差异。

下面的规则将指导您绕过头文件使用中的各种陷阱。

define用法

所有头文件应该由#define防护,以避免多重包含。符号名称的格式应该是<PROJECT>_<PATH>_<FILE>_H_

为了保证唯一性,它们应根据在项目的源代码树的完整路径。例如,在文件中FOO项目cocos2dx/sprites_nodes/CCSprite.h应具有以下防护:

1
2
3
4
5
6
#ifndef COCOS2DX_SPRITE_NODES_CCSPRITE_H_
#define COCOS2DX_SPRITE_NODES_CCSPRITE_H_
 
...
 
#endif  // COCOS2DX_SPRITE_NODES_CCSPRITE_H_
1
2
// Pragma once is still open for debate
#pragma once

我们在考虑是是否使用#pragma once,我们不确定他能支持所有平台。

前向声明

前向声明普通类可以避免不必要的#includes

定义:“前向声明”是类、函数或者模版的声明,没有定义。用前向声明来替代#include通常应用在客户端代码中。

优点:

  • 不必要的#includes会强制编译器打开更多的文件并处理更多的输入。
  • 不必要的#includes也会导致代码被更经常重新编译,因为头文件修改。

缺点:

  • 不容易确定模版、typedefs、默认参数等的前向声明以及使用声明。
  • 不容易判断对给定的代码该用前向声明还是#include,尤其是当有隐式转换时。极端情况下,用#include代替前向声明会悄悄的改变代码的含义。
  • 在头文件中多个前向声明比#include啰嗦。
  • 前向声明函数或者模版会阻止头文件对APIs做“否则兼容”(otherwise-compatible)修改;例如,扩展参数类型或者添加带有默认值的模版参数。
  • 前向声明std命名空间的符号通常会产生不确定的行为。
  • 为了前向声明而结构化代码(例如,适用指针成员,而不是对象成员)会使代码更慢更复杂。
  • 前向声明的实际效率提升未经证实。

结论:

  • 使用头文件中声明的函数,总是#include该头文件。
  • 使用类模版,优先使用#include
  • 使用普通类,可以用前向声明,但是注意前向声明可能不够或者不正确的情况;如果有疑问,就用#include
  • 不应只是为了避免#include而用指针成员代替数据成员。

总是#include实际声明/定义的文件;不依赖非直接包含的头文件中间接引入的符号。例外是,Myfile.cpp可以依赖Myfile.h中的#include和前向声明。

内联函数

只在函数体很小——10行代码以内——的时候将其定义为内联函数。

定义:你可以在声明函数时允许编译器将其扩展内联,而不是通过常见的函数调用机制调用。

优点: 内联短小精悍的函数可以生成更高效的对象码。推荐内联取值函数、设值函数以及其余性能关键的短函数。

缺点: 滥用内联可能导致程序更慢。内联可能让代码尺寸增加或者减少,这取决于函数的尺寸。内联一个非常小的取值函数通常会减少代码尺寸,而内联一个非常大的函数会显著增加代码尺寸。在现代处理器架构下,更小尺寸的代码因为可以更好的利用指令缓存,通常跑得更快。

结论:一个黄金法则是不要内联超过10行的函数。要小心析构函数,因为隐含成员和基类的析构函数,它们通常比看上去的要长。

另一个黄金法则:通常不建议内联带循环或者switch语句的函数(除非,大部分情况下,循环或者switch语句不会被执行)

需要注意的是,即便函数被声明为内联他们也不一定会真的内联;例如虚函数以及递归函数一般都不会被内联。通常递归函数不应该被内联。将虚函数内联的主要原因是为了方便或者文档需要,将其定义放在类中,例如取值函数以及设值函数。

-inl.h文件

如果有需要,可以用带-inl.h后缀的文件来定义复杂内联函数。

内联函数的定义必须放在头文件中,这样编译器在函数调用处内联展开时才有函数定义可用。但实现代码通常还是放在.cpp文件比较合适,因为除非会带来可读性或者性能上的好处,否则我们不希望在.h文件里堆放太多具体的代码。

如果一个内联函数的定义非常短,只含有少量逻辑,你可以把代码放在你的.h文件里。例如取值函数与设值函数都毫无疑问的应该放在类定义中。更复杂的内联函数为了实现者和调用者的方便,也要放在.h文件里,但是如果这样会让.h文件过于臃肿,你也可以将其放在一个单独的-inl.h文件里。这样可以将具体实现与类定义分开,同时又确保了实现在需要用到的时候是被包含的。

-inl.h文件还有一个用途是存放函数模板的定义。这样可以让你的模板定义更加易读。

不要忘记,就像其他的头文件一样,一个-inl.h文件也是需要#define防护的。

函数参数顺序

定义函数时,参数顺序应该为:输入,然后是输出。

C/C++函数的参数要么是对函数的输入,要么是函数给出的输出,要么两者兼是。输入参数通常是值或者常引用,而输出以及输入/输出参数是非const指针。在给函数参数排序时,将所有仅输入用的参数放在一切输出参数的前面。特别需要注意的是,在加新参数时不要因为它们是新的就直接加到最后去;新的仅输入用参数仍然要放到输出参数前。

这不是一条不可动摇的铁律。那些既用于输入又用于输出的参数(通常是类/结构体)通常会把水搅浑,同时,为了保持相关函数的一致性,有时也会使你违背这条原则。

include的命名和顺序

使用以下标准顺序以增加可读性,同时避免隐藏的依赖关系:C库,C++库,其他库的.h文件,你自己项目的.h文件。

所有本项目的头文件都应该包含从源代码根目录开始的完整路径,而不要使用UNIX的目录快捷方式.(当前目录)或者..(上层目录)。例如google-awesome-project/src/base/logging.h应写为以下方式

1
#include "base/logging.h"

例如有文件dir/foo.cppdir/foo_test.cpp,他们的主要用途是实现或者测试dir2/foo2.h头文件里的内容,那么include的顺序应该如下:

  • dir2/foo2.h (推荐位置——理由见后)
  • C system files.
  • C++ system files.
  • Other libraries' .h files.
  • Your project's .h files.

按照这个推荐顺序,如果dir2/foo2.h漏掉了什么必须的包含文件,dir/foo.cpp或者dir/foo_test.cpp就会编译失败。这样的规则就保证了工作在这些文件的人而不是在其他包工作的无辜的人最先发现问题。

dir/foo.cppdir2/foo2.h通常位于同一个目录(例如base/basictypes_test.cppbase/basictypes.h),但是在不同目录也没问题。

在同一部分中包含文件应该按照字母顺序排列。注意如果老代码不符合这条规则,那就在方便的时候改过来。

例如cocos2dx/sprite_nodes/CCSprite.cpp的include部分可能如下:

1
2
3
4
5
6
7
8
9
10
#include "sprite_nodes/CCSprite.h"  // Preferred location.
 
#include <sys/types.h>
#include <unistd.h>
#include <hash_map>
#include <vector>
 
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"

特例:有时候系统相关代码需要使用条件包含。这种情况下可以把条件包含放在最后。当然,要保持系统相关代码短小精悍并做好本地化。例如:

1
2
3
4
5
6
7
8
#include "foo/public/fooserver.h"
 
#include "base/port.h"
 
// For LANG_CXX11.
#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

作用域

命名空间

在.cpp文件中,提倡使用未命名的命名空间(unnamed namespaces,注:未命名的命名空间就像未命名的类一样,似乎被介绍的很少:-()。使用命名的命名空间时,其名称可基于项目的路径名称。不要使用using指示符。不要使用内联命名空间。

定义:命名空间将全局作用域细分为不同的、命名的作用域,可有效防止全局作用域的命名冲突。

优点:命名空间提供了(层次化的)命名轴(name axis,注:将命名分割在不同命名空间内),当然,类也提供了(层次化的)的命名轴。

举例来说,两个不同项目的全局作用域都有一个类Foo,这样在编译或运行时造成冲突。如果每个项目将代码置于不同命名空间中,project1::Foo和project2::Foo作为不同符号自然不会冲突。

内联命令空间自动地将名字置于封闭作用域。例子如下:

1
2
3
4
5
namespace X {
inline namespace Y {
  void foo();
}
}

X::Y::foo()X::foo()是一样的。内联命名空间是为了兼容不同版本的ABI而做的扩展。

缺点:命名空间具有迷惑性,因为它们和类一样提供了额外的(层次化的)命名轴。

特别是内联命名空间,因为命名实际上并不局限于他们声明的命名空间。只有作为较大的版本控制策略的一部分时才有用。

在头文件中使用未命名的空间容易违背C++的唯一定义原则(One Definition Rule, ODR)。

结论:根据下文将要提到的策略合理使用命名空间。如例子中那样结束命名空间时进行注释。

未命名空间

允许甚至鼓励在.cpp中使用未命名空间,以避免运行时的命名冲突:

1
2
3
4
5
6
7
namespace {                           // This is in a .cpp file.
 
// The content of a namespace is not indented
enum { UNUSED, EOF, ERROR };         // Commonly used tokens.
bool atEof() { return _pos == EOF; }  // Uses our namespace's EOF.
 
}  // namespace

然而,与特定类关联的文件作用域声明在该类中被声明为类型、静态数据成员戒静态成员函数,而不是未命名命名空间的成员。不能在.h文件中使用未命名空间。

命名空间

命名的命名空间使用规则如下:

命名空间将除文件包含、全局标识的声明/定义以及类的前置声明外的整个源文件封装起来,以同其他命名空间相区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// .h文件
// 使用cocos2d命名空间
NS_CC_BEGIN
 
// 所有声明均在命名空间作用域内。
// 注意不用缩进。
class MyClass
{
public:
    ...
    void foo();
};
 
NS_CC_END
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// .h文件
// 不使用cocos2d命名空间
namespace mynamespace {
 
// 所有声明均在命名空间作用域中。
// 注意不用缩进。
class MyClass
{
public:
    ...
    void foo();
};
 
}  // namespace mynamespace
1
2
3
4
5
6
7
8
9
10
// .cpp文件
namespace mynamespace {
 
// 函数定义在命名空间作用域中。
void MyClass::foo()
{
    ...
}
 
}  // namespace mynamespace

通常.cpp文件会包含更多、更复杂的细节,包括引用其他命名空间中的类等。

1
2
3
4
5
6
7
8
9
10
11
12
#include "a.h"
 
DEFINE_bool(someflag, false"dummy flag");
 
class C;  // 前向声明全局作用域中的类C。
namespace a { class A; }  // 前向声明a::A。
 
namespace b {
 
...code for b...         // 代码无缩进。
 
}  // namespace b
  • 不要声明std命名空间里的任何内容,包括标准库类的前置声明。声明std里的实体会导致不明确的行为,例如,不可移植。包含对应的头文件来声明标准库里的实体。最好不要使用using指示符,以保证命名空间下的所有名称都可以正常使用。
1
2
// 禁止--污染了命名空间。
using namespace foo;
  • .h的函数、方法、类,.cpp的任何地方都可以使用using声明。
1
2
3
// 在.cpp中没有问题。
// 在.h中必须在函数、方法或者累中。
using ::foo::bar;
  • .h的函数、方法或包含整个.h的命名的命名空间中以及.cpp中,可以使用命名空间别名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// .cpp中一些常用名的缩写
namespace fbz = ::foo::bar::baz;
 
// .h中一些常用名的缩写
namespace librarian {
// 包括该头文件(在librarian命名空间中)在内的所有文件都可以使用下面的别名:
// 因此同一个项目中的别名应该保持一致。
namespace pd_s = ::pipeline_diagnostics::sidetable;
 
inline void myInlineFunction() {
// 函数或者方法中的本地命名空间别名。
namespace fbz = ::foo::bar::baz;
...
}
}  // namespace librarian

注意,.h文件中的别名对所有包含该文件的所有文件都可见,因此公共的头文件(在项目外仍可用)以及通过他们间接办好的头文件应避免定义别名,为了保持公共的APIs尽可能小。

  • 不要用内联命名空间。

嵌套类

当公开嵌套类作为接口的一部分时,虽然可以直接将他们保持在全局作用域中,但将嵌套类的声明置于命名空间中是更好的选择。

定义:可以在一个类中定义另一个类,嵌套类也称成员类(member class)。

1
2
3
4
5
6
7
8
9
class Foo
{
private:
    // Bar是嵌套在Foo中的成员类
    class Bar
    {
       ...
    };
};

优点:当嵌套(成员)类只在被嵌套类(enclosing class)中使用时很有用,将其置于被嵌套类作用域作为被嵌套类的成员不会污染其他作用域同名类。可在被嵌套类中前置声明嵌套类,在.cpp文件中定义嵌套类,避免在被嵌套类声明中包含嵌套类的定义,因为嵌套类的定义通常只与实现相关。

缺点:只能在被嵌套类的定义中才能前置声明嵌套类。因此,任何使用Foo::Bar*指针的头文件必须包含整个Foo类的声明。

结论:不要将嵌套类定义为public,除非它们是接口的一部分,比如,某方法使用了这个类的一系列选项。

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

优先使用命名空间中的非成员函数或者静态成员函数,尽可能不使用全局函数。

优点:某些情况下,非成员函数和静态成员函数是非常有用的,将非成员函数置于命名空间中可避免污染全局命名空间。

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

结论:有时,不把函数限定在类的实体中是有益的,甚至是必要的,要么作为静态成员,要么作为非成员函数。非成员函数不应依赖外部发量,并尽量置于某个命名空间中。相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类,不如使用命名空间。

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

如果你确实需要定义非成员函数,又只是在.cpp中使用,可使用未命名的命名空间或静态关联(如static int Foo() {...})限定其作用域。

局部变量

尽可能缩小函数变量的作用域,并在声明变量时将其初始化。

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

1
2
3
4
5
6
7
8
9
10
int i;
i = f();      // // 坏——初始化和声明分离
 
int j = g();  // // 好——声明时初始化
 
vector<int> v;
v.push_back(1);  // 优先使用括号初始化。
v.push_back(2);
 
vector<int> v = {1, 2};  // 好-v有初始化。

注意:gcc可正确实现了for (int i = 0; i < 10; ++i)(i的作用域仅限for循环),因此其他for循环中可重用iifwhile等语句中,作用域声明(scope declaration)同样是正确的。

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

注意:如果变量是一个对象,每次进入作用域都要调用其构造函数,每次退出作用域都要调用其析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
// 低效实现
for (int i = 0; i < 1000000; ++i) {
    Foo f;  // My ctor and dtor get called 1000000 times each.
    f.doSomething(i);
}
 
//类似变量放到循环作用域外面声明要高效的多:
 
Foo f;  // My ctor and dtor get called once each.
for (int i = 0; i < 1000000; ++i) {
    f.doSomething(i);
}

静态变量和全局变量

class类型的全局变量是被禁止的:这导致隐藏很深的bugs,因为构造和析构的顺序不明确。然而,允许constexpr类型的静态或全局变量:他们没有动态的初始化或者析构。

包含静态存储的对象,包括全局变量,静态变量,全局类成员变量,以及函数静态变量,必须是POD类型(Plain Old Data):只能是POD类型的整形(int)、字符(char)、浮点(float)或者指针或者数组/结构体

对于静态变量,C++只定义了类构造和初始化的部分顺序,并且每次生成的结果可能不一样,这将导致隐藏很深的bugs。因此,除了禁用class类型的全局变量,也不允许使用函数的结果初始化静态POD变量,除非函数(如getenv(),getpid())本身不依赖任何其他的全局变量。

同样,全局变量和静态变量在程序终止时销毁,不管是因为main()返回还是调用了exit()。析构顺序和构造顺序刚好相反,因此析构顺序和构造顺序一样都是不明确的。例如,在程序结束时,静态变量可能已经销毁,但是仍在运行的代码-可能在另外一个线程-试图访问它,然后失败了。或者一个静态string变量先于另外一个包含该字符串的变量执行析构函数。

一个缓解析构函数问题的方法是调用quick_exit()而不是exit()来终止程序。区别是quick_exit()不调用析构函数,也不引入在atexit()中注册的任何句柄。如果程序终止时有句柄需要通过quick_exit()来运行(比如,刷新日志),在at_quick_exit()中注册它。(如果句柄需要在exit()quick_exit()中都运行,那就在两个地方都注册)。

综上所述,只允许静态变量包含POD数据。禁用vector(用C数组代替),禁用string(用const char []代替)。

如果确实需要class类型的静态或者全局变量,考虑初始化一个指针(永不释放),要么在main()函数中,要么在pthread_once()中。注意指针必须是原始指针,不能是“智能”指针,因为智能指针的析构函数有我们一直在避免的析构函数顺序问题。

类是C++代码中最根本的单元。很自然地,我们会经常用到类。本节列出了在写类的时候应该遵循的一些该做和不应该做的事情。

在构造函数里面完成工作

在构造函数里面避免复杂的初始化(特别是那些初始化的时候可能会失败或者需要调用虚拟函数的情况)

定义:有可能在构造函数体内执行初始化

优点:方便书写。不必要担心类是否已经被初始化。

缺点:在构造函数里完成工作面临如下问题:

  • 由于缺少异常处理(在构造函数中禁止使用),构造函数很难去定位错误。
  • 如果初始化失败,接着我们继续使用一个初始化失败的对象,可能会出现不可以预知的状态。
  • 如果初始化调用了虚拟函数,这些调用将不会正确的传至子类的实现。以后对该类的修改可能会悄悄的出现该问题,即使你的类当前并不是子类,也会引起混乱。
  • 如果创建一个该类的全局变量(虽然违反规则,但是仍然有人会这样子做),构造函数代码会在main函数之前被调用,可能会破坏一些在构造函数代码里面隐含的假设,譬如,gflags还没有被初始化。

结论: 构造函数不应该调用虚函数,否则会引起非致命的错误。如果你的对象需要的初始化工作比较重要,你可以考虑使用工厂方法或者Init()方法。

初始化

如果你的类定义了成员变量,你必须在类里面为每一个成员变量提供初始化或者写一个默认的构造函数。如果你没有声明任何构造函数,编译器会为你生成一个默认的构造函数,这个默认构造函数可能没有初始化一些字段,也可能初始化为不恰当的值。

定义: 当我们以无参数形式new一个类对象的时候会调用默认构造函数。当调用‘new[]’(用于创建数组)的时候默认构造函数总是会被调用。在类成员里面进行初始化是指声明一个成员变量的时候使用一个结构例如‘int _count = 17’或者‘string _name{"abc"}’来替代这样的形式‘int _count’或者‘string _name’

优点:如果没有提供初始化的操作,一个用户定义的默认构造函数是用来初始化一个对象。它能保证一个对象被创建后总是处于有效或者可用状态;它也能保证一个对象时在最初被创建的时候处于一个明显不可能出现的状态来简化调试。在类里面的成员进行初始化工作能保证一个成员变量正确的被初始化且不会出现在多个构造函数有同样的初始化代码。这样在你新增一个成员变量的时候就就可以减少出现bug的几率,因为你可能记得了在某一个构造函数里面初始化它了,却忘了在其他构造函数里面进行初始化。

缺点:对于开发者,明确地定义一个默认构造函数是一个额外工作。在对类成员进行初始化工作时如果一个成员变量在声明时初始化同时也在构造函数里面初始化,这可能会引起混乱,因为在构造函数里面的值会替换掉在声明时的值。

结论:使用类成员初始化作为简单的初始化,特别是当一个成员变量在多个构造函数里面必须使用相同的方式初始化的时候。如果你的类定义了成员变量是没有在类里面进行初始化的,且如果没有其它构造函数,你必须定义一个无参数的默认构造函数。它应该使用保持内部状态一致和有效的方式来更好的初始化类对象。原因是因为如果你没有其他构造函数且没有定义一个默认的构造函数,编译器会生成同一个默认的构造函数给你。编译器生成的构造函数对你的对象的初始化可能并不正确。如果你的类继承自一个已经存在的类,但是你并没有添加新的成员变量,你就不需要默认构造函数了。

显式构造函数

对只有一个参数的构造函数使用C++关键字explicit

定义:一般来说,如果一个构造函数只有一个参数,它可以当做转换函数使用。例如,如果你定义了Foo::Foo(string name),然后传进一个string类型给一个函数是需要Foo类型的,Foo的构造函数将会被调用并转换这个string类型为Foo类型,然后把这个Foo类型传递给这个函数。这能提供便利,但是这也是产生麻烦的根源:当一个对象被转换了,但是它却不是你想要的类型。显式地声明一个构造函数可以防止这种隐性转换。

优点:避免出现不合需求的转换

缺点:没有

结论:所有的单个参数的构造函数都应该使用explicit显式声明。在定义类的时候,对于只有一个参数的构造函数时总是要在其前面使用explicit:explicit Foo(string name);

有一点例外的是拷贝构造函数,在一些比较少的情况我们允许它不使用explicit。还有一种例外的情况是,那些打算作为透明封装的类。这两种情况都应该明确的进行注释。

最后,构造函数中只有一个初始化列表的可以是非explicit。这是为了允许你的类型结构可以使用大括号初始列表的方式进行赋值。

拷贝构造函数

当需要时应该提供一个拷贝构造函数和赋值操作符。否则,使用DISALLOW_COPY_AND_ASSIGN来禁用它们

定义:拷贝构造函数和赋值操作符是用来创建一个对象的拷贝。拷贝构造函数是有编译器在某些情况下隐式调用的,例如,以传值方式传一个对象的时候。

优点:拷贝构造韩式使得拷贝对象变得简单。STL容器要求所有的内容都是可以拷贝和赋值的。拷贝构造函数比CopyFrom()方式这种替代方案更高效,在某些情况,编译器可以省去它们,它也避免了堆分配的开销。

缺点:在C++中,隐式的拷贝对象可能会引起bugs和性能问题的。它也会减少可读性,因为它使得以传值方式的对象难以跟踪,相对于传引用来说,对象的改变会立刻得到反馈。

结论:很少类需要能被拷贝。大部分类时不需要拷贝构造函数和赋值操作符的。在许多情况下,一个指针或者引用和一个被拷贝的值使用起来是差不多的,然而它们却更高效。例如,你可以以引用或者指针的方式传递函数参数来代替传值方式,你也可以用指针替代类对象来保存在STL容器里面。

如果你的类需要能被拷贝,与其提供一个拷贝构造函数,不如提供提供一个例如clone()的拷贝函数更好,因为这样的函数不能被隐式的调用。如果一个拷贝方法不满足你的需求情况(例如,考虑到性能原因,或者你的类需要以值方式保存在STL容器里面),可以也再提供拷贝构造函数和赋值操作符。

如果你的类不需要拷贝构造函数或者赋值操作符,你必须显式禁用它们。你可以这样子做,在类里面以私有方式为拷贝构造函数和赋值操作符添加声明,记得不要对它们提供任何对应的实现(这样会导致链接错误)。

为了方便,可以这样定义一个DISALLOW_COPY_AND_ASSIGN宏:

1
2
3
4
5
// A macro to disallow the copy constructor and operator= functions
// This should be used in the private: declarations for a class
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
  TypeName(const TypeName&);               \
  void operator=(const TypeName&)

然后,在类Foo里面的实现就可以这样:

1
2
3
4
5
6
7
8
9
class Foo
{
public:
    Foo(int f);
    ~Foo();
 
private:
    DISALLOW_COPY_AND_ASSIGN(Foo);
};

委派和继承构造函数

可以减少重复代码时使用委派和继承构造函数。

定义:

委派构造函数和继承构造函数是为了减少构造函数重复代码而在C++11中引入的两个不同的特性。委派构造函数允许类的一个构造函数通过特殊的初始化列表语法调用另外的构造函数。

1
2
3
4
5
X::X(const string& name) : name_(name) {
  ...
}
 
X::X() : X("") { }

继承构造函数允许派生类可以直接使用基类的构造函数,就像使用基类的其他成员函数,而不需要重新声明这些构造函数。尤其当基类有多个构造函数时特别有用。

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
    Base();
    Base(int n);
    Base(const string& s);
    ...
};
 
class Derived : public Base {
public:
    using Base::Base;  // Base's constructors are redeclared here.
};

当派生类构造函数仅仅只是调用基类构造函数时特别有用。

优点:

委派构造函数和继承构造函数可以减少冗余代码,从而提高代码可读性。

Java程序员对委派构造函数很熟悉。

缺点:

使用辅助函数可以预估委派构造函数的行为。

如果派生类引入了新的成员变量,那么继承构造函数会被迷惑,因为基类构造函数不知道这些新的成员变量。

结论:

当可以减少冗余、提高可读性的时候使用委派构造函数和继承构造函数。当派生类有新的成员变量的时候谨慎对待继承构造函数。如果派生类成员变量使用类内成员初始化(in-class member initialization),继承构造函数仍然是适用的。

结构体 vs 类

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

在C++中,关键字structclass几乎含义等同,我们为其人为添加语义,以便为定义的数据类型合理选择使用哪个关键字。

struct被用在仅包含数据的消极对象(passive objects)上,可能包括有关联的常量,但没有存取数据成员之外的函数功能,而存取功能通过直接访问实现而无需方法调用,这里提到的方法是指只用于s处理数据成员的,如构造函数、析构函数、Initialize()、Reset()、Validate()。

如果需要更多的函数功能,class更适合,如果不确定的话,直接使用class

为了与STL保持一直,仿函数(functors)和特性(traits)可以不用class而是使用struct

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

继承

使用组合(composition,注,这一点也是GoF在《Design Patterns》里反复强调的)通常比使用继承更适宜,如果使用继承的话,只使用公共继承。

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

优点:实现继承通过原封不动的重用基类代码减少了代码量。由于继承是编译时声明(compile-time declaration),编码者和编译器都可以理解相应操作并发现错误。接口继承可用于程序上增强类的特定API的功能,在类没有定义API的必要实现时,编译器同样可以侦错。

缺点:对于实现继承,由于实现子类的代码在父类和子类间延展,要理解其实现变得更加困难。子类不能重写父类的非虚函数,当然也就不能修改其实现。基类也可能定义了一些数据成员,用于区分基类的物理布局(physical layout)

结论:所有继承必须是public的,如果想私有继承的话,应该采取包含基类实例作为成员的方式作为替代。

不要滥用实现继承,组合通常更加合适。努力做到只在“是一个”("is-a",译者注,其他"has-a"情况下请使用组合)的情况下使用继承:如果Bar的确“是一种”Foo,才令Bar是Foo的子类。

必要的话,令析构函数为virtual,这里必要是指该类具有虚函数。

限定仅在子类访问的成员函数为protected,需要注意的是数据成员应始终为私有。

当重定义派生的虚函数时,在派生类中明确声明其为virtual。根本原因:如果遗漏virtual,读者需要检索类的所有祖先以确定该函数是否为虚函数(注,虽然不影响其为虚函数的本质)。

多重继承

真正需要用到多重实现继承(multiple implementation inheritance)的时候非常少,只有当最多一个基类中含有实现,其他基类都是以Interface为后缀的纯接口类时才会使用多重继承。

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

优点:相比单继承,多重实现继承可令你重用更多代码(参考继承章节)。

缺点:真正需要用到多重实现继承的时候非常少。当多重实现继承看上去是不错的解决方案时,通常可以找到更加明确、清晰的、不同的解决方案。

结论:只有当所有超类(superclass)除第一个外都是纯接口类时才能使用多重继承。为确保它们是纯接口,类必须以Interface为后缀。

注意:关于此规则,Windows下有种例外情况(译者注,将在本译文最后一篇的例外规则中阐述)。

接口

接口是指满足特定条件的类,这些类以Interface为后缀(非必需)。

定义:

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

  • 只有纯虚函数("=0")和静态函数(下文提到的析构函数除外);
  • 没有非静态数据成员;
  • 没有定义任何构造函数。如果有,也不含参数,并且为protected
  • 如果是子类,也只能继承满足上述条件并且后缀是Interface的类。

接口类不能被直接实例化,因为它声明了纯虚函数。为确保接口类的所有实现可被正确销毁,必须为之声明虚析构函数(作为第1条规则的例外,析构函数不能是纯虚函数)。具体细节可参考Stroustrup的《The C++ Programming Language, 3rd edition》第12.4节。

优点:Interface为后缀可令他人知道不能为该接口类增加实现函数或非静态数据成员,这一点对多重继承尤其重要。另外,对于Java程序员来说,接口的概念已经深入人心。

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

结论:只有满足上述需要,类才可能以Interface结尾,但反过来,满足上述需要的类未必一定以Interface结尾。

操作符重载

除少数特定环境外,不要重载操作符。

定义:一个类可以定义诸如+、/等操作符,使其可以像内建类型一样直接使用。重载操作符""允许使用内置文本语法来创建类的对象。

优点:操作符重载使代码看上去更加直观,就像内建类型(如int)那样。相比Equals()Add()等黯淡无光的函数名,操作符重载有趣多了。

为了使一些模板函数正确工作,你可能需要定义操作符。

自定义的文本是一个非常简洁的符号,用来创建用户自定义类型的对象。

缺点:虽然操作符重载令代码更加直观,但也有以下不足:

  • 混淆直觉,让你误以为一些耗时的操作像内建操作那样轻巧;
  • 查找重载操作符的调用处更加困难,查找Equals()显然比==容易的多;
  • 有的操作符可以对指针进行操作,容易导致bugs。Foo + 4做的是一件事,而&Foo + 4可能做的是完全不同的另一件事,对于二者,编译器都不会报错,使其很难调试;
  • 即便是对于老道的C++程序员,用户自定义文创建新的语法形式也是一件很陌生的事情。

重载还有令你吃惊的副作用,比如,前置声明重载操作符&的类很不安全。

结论:一般不要重载操作符,尤其是赋值操作(operator=)暗藏杀机,应避免重载。如果需要的话,可以定义类似Equals()CopyFrom()等函数。同样的,不惜一切代价避免重载一元操作符&,如果类有可能被前向声明的话。

不要重载操作符"",比如,不要引入自定义文本。

然而,极少数情况下需要重载操作符以便与模板或“标准”C++类衔接(如operator<<(ostream&, const T&)),如果被充分证明则是可接受的,但你仍要尽可能避免这样做。尤其是不要仅仅为了在STL容器中作为key使用就重载operator==operator<,取而代之,你应该在声明容器的时候,创建相等判断和大小比较的仿函数类型。

有些STL算法确实需要重载operator==时可以这么做,但是不要忘了提供文档说明原因。

亦可参考拷贝构造函数函数重载章节。

访问控制

将数据成员私有化,并提供相关访问函数(因技术原因,当使用Google测试时,允许test类中的数据成员是protected)。典型得,变量命名为_foo,取值函数为getFoo(),赋值函数为setFoo()例外:静态常量数据成员(命名为FOO)不需要是private

取值函数一般作为内联函数定义在头文件中。

亦可参考继承函数名称章节。

声明顺序

在类中使用特定的声明顺序:public:private:之前,成员函数在数据成员(变量)之前等等。

类的各部分定义顺序如下:首先是public:部分,然后是protected:部分,最后是private:部分。如果其中某部分没有,直接忽略即可。

在上述任何部分内,声明需要遵循以下顺序:

  • TypedefsEnums
  • 常量(static const类型的数据成员)
  • 创建函数(createXXX方法)
  • 构造函数
  • 析构函数
  • 成员方法,包括静态方法
  • 重写方法(overridden methods,必须以override关键字作为后缀)
  • 数据成员(static const数据成员除外)

友元声明必须放在private:部分,宏DISALLOW_COPY_AND_ASSIGN应该放在private:部分最后。这应该是类的最后一部分内容。亦可参考拷贝构造函数章节。

.cpp文件中函数的定义应尽可能和声明次序一致。

不要在类的定义中内联大型函数定义。通常,只有那些没有特别意义的或者性能要求高的,并且比较短小的函数才被定义为内联函数。更多细节参考内联函数章节。

示例:

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
26
27
28
29
30
31
32
33
34
class MyNode : public Node
{
// public first
public:
    // "creator" methods first
    static MyNode *create();
    static MyNode *createWithParam(const something& arg);
 
    // If applicable, then Constructors and the Destructor   
    MyNode();
    virtual ~MyNode();
 
    // Then the init methods
    bool init();
    bool initWithParam(const something& arg);
 
    // Then methods of the instance
    void addColor( const Color& color );
    void addBackgroundImage( const char* filepath );
 
    // Then the overrides
    virtual void visit(void) override;
    virtual void addChild(Node * child, int zOrder, int tag) override;
 
// then protected
protected:
    Point absolutePosition();
 
// then private
private:
    Point    _lastPosition;
};
 
}

编写短函数

优先选择短小、精炼的函数。

长函数有时是恰当的,因此函数长度没有严格限制。但是如果函数超过40行,可以考虑在不影响程序结构的情况下将其分割一下。

即使一个长函数现在工作的非常完美,别人仍可能为其添加新的行为,这可能导致难以发现的bugs。保持函数短小、简单,方便他人阅读和修改代码。

有时你可能会碰到复杂的长函数。不要害怕修改现有代码:如果证实这些代码难于使用、调试,或者你需要使用其中的一小块功能。考虑将其分割为更加短小、易于管理的若干函数。

其它C++特性

所有权和智能指针

关于所有权和智能智能指针,最好使得动态分配的对象有单一、固定的所有者。最好用智能指针来转移所有权。

定义

“所有权”是管理动态分配的内存(还有其它资源)的一种簿记(bookkeeping)技术。动态分配对象的所有者是一个对象或者函数,这个对象或者函数负责在动态分配对象不再需要时将其删除。所有权有时可以共享,在这种情况下,最后一个所有者通常负责将其删除。即使所有权不是共享的,它也可以通过代码段来转移。

“智能”指针看起来像指针,比如通过重载*和 - >运算符。一些智能指针类型可用于自动化簿记所有权,通过自动化来确保所有权的上述责任可以得到满足。std::unique_ptr是C++11中介绍的一种智能指针类型,它表达了动态分配对象的独占所有权,在std::unique_ptr指针超出范围时这个对象被删除。这个对象不能被复制,但是可以将所有权转移。shared_ptr也是一种智能指针类型,它表达了动态分配对象的共享所有权。shared_ptrs可以被复制,并且对象的所有权被所有副本共享,当最后一个shared_ptr被销毁时对象被删除。

优点:

  • 在没有所有权逻辑的情况下不可能管理动态内存分配.
  • 相比起复制来说,转移对象的所有权消耗的资源更小(在可以复制的情况下).
  • 所有权转移比起’借用’(borrowing)指针或者引用更简单一些,因为不再需要在两个使用者之间协调对象的生命周期.
  • 使所有权逻辑清晰、自文档化、引用明确可以提高智能指针的可读性.
  • 智能指针可以消除所有权的主动簿记,简化代码,并且可以排除常见类型的错误.
  • 对于常量对象,共享所有权是替代深拷贝的简单有效的方法.

缺点:

  • 所有权必须通过指针来表示和转移(无论智能或者普通指针)。指针语义比普通值的语义要更复杂,尤其在API中: 不仅需要考虑到所有权,而且还要考虑对象引用混淆(aliasing)、生命周期、可变性等其它的问题.
  • 值语义(value semantics)的性能成本经常被高估,因此所有权转移的性能优点可能无法证明它的可读性和复杂性成本.
  • 所有权转移操作相关的API会强制将它们的操作源引入一个单一内存管理的模式.
  • 当资源释放发生的时候,使用智能指针的代码会有些不太明确.
  • std::unique_ptr在表示所有权转移时用的是C++11的操作语义,这在Google代码中是禁止的,这样使用的话可能会让一些程序员产生混淆.
  • 共享所有权操作会比所有权的谨慎设计更有诱惑性,这样处理会模糊系统的设计.
  • 所有权共享在系统运行时需要明确的簿记,这样的操作开销会比较大.
  • 在某些情况下(比如循环引用),共享所有权的对象可能会永远得不到释放删除.
  • 智能指针不是普通指针的完美替代.

结论:

如果必须要动态分配对象,那么最好让分配所有权的代码一直持有所有权。如果其它代码需要访问持有所有权的对象,可以考虑不传递所有权而是传递一个副本、或者一个指针或引用。最好使用std::unique_ptr使得所有权的传递更明确。比如:

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

在不是很必要的情况下不要在你的代码中使用所有权共享。其中一种情况是为了避免复制操作的高昂开销,但是你应该只在性能优势提高很显著的情况下使用,并且底层的对象是不可变的(即shared_ptr<const Foo>)。如果要使用所有权共享,最好使用shared_ptr.

除非是为了跟老版本的C++兼容,否在在新版本的C++代码中的不要使用scoped_ptr,永远不要使用linked_ptr 或者std::auto_ptr。在所有以上这三种情况下,用std::unique_ptr来代替.

引用参数

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

定义

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

优点:

定义形参为引用避免了像(*pval)++这样难看的代码,像拷贝构造函数这样的应用也是必须的。而且很清楚,不像指针那样不能使用空指针null。

缺点:

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

结论:

函数形参表中,所有的引用必须是const:

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

这是一个硬性约定:输入参数是值或者常数引用,输出参数为指针。输入参数可以是常数指针,但不能使用非常数引用形参,除非是约定需要,比如swap() 。

不过,有些情况下选择输入形参时,const T*const T&更好。

例如:

  • 需要传递一个空指针。
  • 函数保存了一个指针或者引用作为输入。
  • 传递const char*给字符串。

要记住,大多数情况下输入形参要被指定为const T&。使用 const T*会传达给读者这样一个信息:输入参数要以某种方式区别处理。因此有确切的理由时,再选择const T*而不是const T&作为形参输入,否则会误导读者去寻找有关这方面其实不存在的解释。

右值引用

不要使用右值引用,std::forwad,std::move_iteratorstd::move_if_noexcept。仅与不可拷贝参数一起使用std::move的单参数形式。

定义:右值引用是引用的一种,只能绑定到临时对象。语法与传统的引用语法想死。例如void f(string&& s);声明了一个函数,其参数是一个字符串的右值引用。

优点:

  • 定义移动构造函数(使用类类型右值引用的构造函数)可以用移动值来替代拷贝值。如果v1vector<string>,那么auto v2(std::move(v1))很可能只是一些简单的指针操作而不是拷贝大量的数据。有时这可以显著提升性能。
  • 右值引用可以编写通用的函数封装来转发其参数到另外一个函数,无论其参数是否是临时对象。
  • 右值引用实现了可移动不可拷贝的类型,这对那些拷贝没有意义,但是仍会作为函数参数传递或者塞到容器内等的类型非常有用。
  • std::move对高效使用某些标准库类型如std::unique_ptr来说是必须的。

缺点:

  • 右值引用是一个相对较新的特性(C++11的一部分),还没有被广泛的理解。像引用坍塌(reference collapsing)、移动构造函数的自动推导等规则很复杂。
  • 右值引用鼓励重度使用值语义的编程风格。对很多开发者来说,这种风格是陌生的,而且其性能特点也很难言明。

结论:

禁止使用右值引用,以及std::forwardstd::move_if_noexcept(本质上就是转换到右值类型),或std::move_iterator。只与不可拷贝的对象或者在模版代码中的不可拷贝对象一起使用单参数std::move

函数重载

仅在输入参数类型不同、功能相同时使用重载函数(含构造函数)。

定义:

可以定义一个函数参数类型为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);
};

优点:通过重载不同参数的同名函数,令代码更加直观,模板化代码必须使用重载,同时为读者带来便利。

缺点:限制使用重载的一个原因是在特定调用处很难确定到底调用的是哪个函数,另一个原因是当派生类只重载函数的部分变量会令很多人对继承语义产生困惑。

Decision:如果你想重载一个函数,考虑让函数名包含参数信息,例如,使用AppendString()AppendInt()而不是Append()

缺省参数

禁止使用缺省函数参数,除非是下述有限的几种情况之一。如果合适,用函数重载来替代。

优点:经常一个函数带有缺省值,偶尔会重写一下这些值。缺省参数为极少的例外情况提供了少定义一些函数的方便。相比重载这个函数,缺省参数有更干净的语义,用更少的样板,并且更清晰的区分“必须”和“可选”的参数。

缺点:缺省参数的存在使得函数指针产生迷惑,因为函数的签名与调用的签名经常不一致。往现有的函数中增加缺省参数会改变函数的类型,这会导致使用函数地址的代码出现问题。函数重载可以避免这些问题。而且,缺省参数会导致“笨重”的代码,因为他们在每个调用的地方都被重复,而重载的函数只有在定义的地方才出现“这些”缺省。

结论:尽管上述的缺点并不是那么“繁重”,但是相比缺省参数带来的很小的好处,仍然是得不偿失。因此除了下述的例外,所有的参数都应该显式的指定。

一个特例是当函数是.cpp文件中静态函数(或者在一个未命名的命名空间里)。这种情况下,因为函数只在很小的作用域中使用,缺省参数的缺点就显得微不足道。

通常情况下,cocos2dx的createXXXinitXXX方法允许使用缺省参数。

另外一个特例是缺省参数用于变长参数列表。例如:

1
2
3
4
5
// Support up to 4 params by using a default empty AlphaNum.
string strCat(const AlphaNum &a,
              const AlphaNum &b = gEmptyAlphaNum,
              const AlphaNum &c = gEmptyAlphaNum,
              const AlphaNum &d = gEmptyAlphaNum);

变长数组和alloca

禁止使用变长数组和alloca()

优点:变长数组具有浑然天成的语法。变长数组和alloca()也都很高效。

缺点:变长数组和alloca()不是标准C++的组成部分,更重要的是,它们在堆栈(stack)上根据数据分配大小可能导致难以发现的内存泄漏:“在我的机器上运行的好好的,到了产品中却莫名其妙的挂掉了”。

结论:使用安全的分配器(allocator),如scoped_ptr/scoped_array

友元

允许合理使用友元类及友元函数。

通常将友元定义在同一文件下,避免读者跑到其他文件中查找其对某个类私有成员的使用。经常用到友元的一个地方是将FooBuilder声明为Foo的友元,FooBuilder以便可以正确构造Foo的内部状态,而无需将该状态暴露出来。某些情况下,将一个单元测试用类声明为待测类的友元会很方便。

友元延伸了(但没有打破)类的封装界线,当你希望只允许另一个类访问某个成员时,使用友元有时比将其声明为public要好得多。当然,大多数类应该只提供公共成员与其交互。

异常

禁止使用C++异常。

优点:

  • 异常允许应用的更上层代码决定如何处理在底层嵌套函数中发生的“不可能发生”的失败,不像出错代码的记录那么模糊费解;
  • 应用于其他很多现代语言中,引入异常使得C++与Python、Java及其他和C++相近的语言更加兼容;
  • 有些C++第三方库使用了异常,关闭异常会使继承更加困难;
  • 异常是解决构造函数失败的唯一方案,虽然可以通过工厂函数(factory function)和Init()方法模拟异常,但他们分别需要堆分配或者新的“非法”状态;
  • 在测试框架(testing framework)中,异常确实很好用。

缺点:

  • 在现有函数中添加throw时,必须检查所有调用处,即使它们至少具有基本的异常安全保护,或者程序正常结束,永远不可能捕获该异常。例如:如果f()依次调用了g()h()h抛出被f捕获的异常,g就要当心了,避免出现错误清理;
  • 通俗一点说,异常会导致无法通过查看代码确定程序控制流:函数有可能在不确定的地方返回,从而导致代码管理和调试困难,当然,你可以通过规定何时何地如何使用异常来最小化的降低开销,却给开发人员带来掌插这些规定的负担;
  • 异常安全需要RAII和不同编码实践。轻松的编写异常安全(exception-safe)的代码需要大量的支持机制。进一步,为了避免需要读者去理解整个调用表,异常安全代码必须隔离将持久状态写入到“提交”阶段的逻辑。这样做有利有弊(或许你不得不为了隔离提交而混淆代码)。允许使用异常将会付出这些不值得的代价。
  • 加入异常使二进制文件体积发大,增加了编译时长(或许影响不大),还可能增加地址空间压力;
  • 异常的实用性可能会刺激开发人员在不恰当的时候抛出异常,或者在不安全的地方从异常中恢复,例如,非法用户输入不应该导致抛出异常。如果允许使用异常会使得这样一篇编程风格指南长出很多。

结论:

表面上看,使用异常利大于弊,尤其是在新项目中。然而,对于现有代码,引入异常会牵连到所有依赖的代码。如果允许异常在新项目中使用,在跟以前没有使用异常的代码集成时也是一个麻烦。因为Google现有的大多数C++代码都没有异常处理,引入带有异常处理的新代码相当困难。

鉴于Google现有代码不接受异常,在现有代码中使用异常比在新项目中使用的代价多少要大一点,迁移过程会比较慢,也容易出错。我们也不相信异常的有效替代方案,如错误代码、断言等,都是严重负担。

我们并不是在哲学或道德层面反对使用异常,而是在实践的基础上。因为我们希望使用Google上的开源项目,但项目中使用异常会为此带来不便,因为我们也建议不要在Google上的开源项目中使用异常,如果我们需要把这些项目推倒重来显然不太现实。

这一禁止同样适用于C++11中的异常相关的特性,例如noexceptstd::exception_ptrstd::nested_exception

对于Windows来说,这一点有个例外(没有歧义)。

运行时类型识别

cocos2dx编译、运行都需要RTTI。也就是说,不禁用RTTI,但是要小心使用。

定义:RTTI允许程序员在运行时识别C++类对象的类型。通过使用typeid或者dynamic_cast完成。

缺点:

运行时查询对象的类型通常意味着设计出了问题,而运行时需要知道对象的类型则意味着类的层次有缺陷。

零散地使用RTTI使代码维护变得困难,它使得基于类型的判断树或者switch语句散落在代码中,当以后修改的时候这些代码都必须重新测试。

优点:

RTTI的标准替代方案(后面将描述)需要修改或者重新设计有问题的类层次。有时候,这样的修改是不可行或不可取的,特别是对于使用广泛的或成熟的代码。

RTTI在某些单元测试中非常有用,如在进行工厂类测试时用于检验一个新建对象是否为期望的动态类型。RTTI对于管理对象和派生对象的关系也很有用。

当管理多个抽象对象时,RTTI也很有用。

1
2
3
4
5
6
7
8
bool Base::equal(Base* other) = 0;
bool Derived::equal(Base* other)
{
    Derived* that = dynamic_cast<Derived*>(other);
    if (that == NULL)
        return false;
    ...
}

结论:

RTTI有合法的用途但是容易被滥用,因此你要小心的使用它。在单元测试中你可以随意使用,但是在其他代码中尽可能去避免使用RTTI。特别是在新代码中使用RTTI时,请三思而后行。如果需要根据对象类型来做不同的行为,考虑换一种方案来查询类型:

  • 虚函数可以实现随子类类型不同而执行不同代码,对象本身完成了这项工作。
  • 如果工作在对象之外的代码中完成,考虑双重分发方案,如Visitor模式,可以方便的在对象本身外确定类的类型。

如果程序保证给定的基类实例实际上是某个派生类的实例,那么可以自由使用dynamic_cast。通常这种情况下可以使用static_cast来替代。

基于类型的判断树是一个强烈的信号,指示你的代码行走在错误的轨道上。

1
2
3
4
5
6
if (typeid(*data) == typeid(D1)) {
    ...
else if (typeid(*data) == typeid(D2)) {
    ...
else if (typeid(*data) == typeid(D3)) {
    ...

这样的代码通常在有引入新的子类时崩溃。而且,当某个子类的属性发生改变,很难找到并修改所有受影响的代码。

不要手工实现一个类似RTTI的发难。我们反对使用RTTI,同样反对类似带类型标签的类层次的解决方案。这些解决方案掩盖你的真实意图。

转换

使用C++类型转换,如static_cast<>()。不要用其他形式的转换,如int y = (int)x;或者int y= int(x);

定义: C++引入了一种与C不同的转换系统,可以区分不同类型的转换操作。

优点: C语言类型转换操作很模糊,有时你做的是强制转换(例如,(int)3.5),而有时你做的是类型转换(例如,(int)"hello")。C++的转换则可以避免这种情况。而且C++的转化更利于搜索。

缺点: 语法很讨厌。

结论:

使用C++风格而不要使用C风格类型转换。

  • static_cast和C风格转换相似,既可以做值的强制转换,也可以做子类指针到父类指针的向上类型转换。
  • const_cast用来移除const修饰。
  • reinterpret_cast用来做指针和整形或其他类型指针之间的不安全的相互转换。仅在你对所做的一切了然于心时使用。

dynamic_cast的使用指南请参考运行时类型信息(RTTI)一节。

只在日志中使用流。

定义: 流是printf()scanf()的替代品。

优点: 应用流,你无需知道要打印的对象的类型。格式化字符串时参数不匹配也没有问题。(虽然在gcc中printf()也不存在该问题)。流提供自动的构造函数和西沟函数,用以打开和关闭相关文件。

缺点: 流使得诸如pread()这样的函数难以实现。如果没有类似printf篡改就无法高效的使用流,某些格式化(尤其是常用的字符串格式语法%.*s)也难以实现。流不支持操作符重排序(%1s),而这一点对于国际化很有用。

结论:

除了日志接口,不要使用流,取而代之用类似printf方法。

流有很多利弊,但是代码一致性胜过一切。不过要代码中使用流。

讨论延伸:

在这个问题上一直存在争议,这里给出更深层次的原因。回忆唯一性(Only One Way)原则:我们要确保无论何时我们做某类I/O,代码都保持一致。鉴于此,我们不希望用户来决定使用流还是使用printf+Read/Write/等。相反的,应该由我们来决定到底使用哪种方式。日志是一个例外,那是因为用流非常适合,同时也是因为历史遗留问题。

流的支持者们主张流是不二之选,但是实际并非如此。他们提到的流的每一个优点,也都是其缺点。最大的优点是不需要知道将要打印的对象的类型。确实如此,但是,不足却是:很容易使用错误的类型,编译器也不会发出警告。使用流时很容易犯此类错误。

1
2
cout << this;  // Prints the address
cout << *this;  // Prints the contents

编译器不会报错,因为<<被重载了。就是因为这一点,我们反对操作符重载。

有些人会说printf格式很丑、易读性差,但是流也好不到哪去。看看下面的两段代码,包含相同的笔误,哪个更容易发现?

1
2
3
4
5
6
cerr << "Error connecting to '" << foo->bar()->hostname.first
     << ":" << foo->bar()->hostname.second << ": " << strerror(errno);
 
fprintf(stderr, "Error connecting to '%s:%u: %s",
        foo->bar()->hostname.first, foo->bar()->hostname.second,
        strerror(errno));

你能举出的任何问题都时这样的。(你可能会争辩,“正确的封装会好点”,即便这儿可以,其他地方呢?不要忘了,我们的目标是使语言尽可能小,而不是添加一些别人需要学习的新的内容。)

每一种方式都是各有利弊,“没有最好,只有更好”,简单化的教条告诫我们必须从中选择其一,最后的多数决定是printf + read/write。

前置自增和自减

对于迭代器和其他模板对象使用前缀形式(++i)的自增、自减运算符。

定义:对于变量在自增(++i或i++)或自减(--i或i--)后又没有使用表达式的值的情冴下,需要确定到底是使用前置还是是后置的自增自减。

优点:不考虑返回值的话,前置自增(++i)通常要比后置自增(i++)效率更高,因为后置的自增自减需要对表达式的值i进行一次拷贝,如果i是迭代器戒其他非数值类型,拷贝的代价是比较大的。既然两种自增行为一样(注,不考虑表达式的值,相信你知道我在说什么),为什么不直接使用前置自增呢?

缺点::C语言中,当表达式的值没有使用时,传统的做法是使用后置自增,特别是在for循环中,有些人觉得后置自增更加易懂,因为这很像自然语言,主语(i)在谓语动词(++)前。

结论:对简单数值(非对象)来说,两种都无所谓,对迭代器和模板类型来说,要使用前置自增(自减)。

const用法

我们强烈建议你在任何可以使用的情况下都要使用const。C++11中,有些情况下使用constexpr更好些。

定义:在声明的发量或参数前加上关键字const用于指明变量值不可修改(如const int foo),为类中的函数加上const修饰表明该函数不会修改类成员变量的状态(如class Foo { int Bar(char c) const; };)。

优点:便于理解变量的用法;允许编译器更好的进行类型检查、生成更优质的代码;帮助大家更自信的编写正确的代码,因为我们知道所调用的函数被限制了能或者不能修改变量值;使大家知道在无锁的多线程中哪些函数是安全的。

缺点:如果你向一个函数传入const变量,函数原型中也必须是const的(否则变量需要const_cast类型转换),在调用库函数时这尤其麻烦。

结论:

const变量、数据成员、函数和参数为编译时类型检测增加了一层保障,更有利于尽早发现错误。因此,我们强烈建议在任何可以使用的情况下使用const

  • 如果函数不会修改引用或者指针参数,那么参数应该是const的。
  • 尽可能将函数声明为const,访问函数应该总是const,其他函数如果不会修改任何数据成员也应该是const,不要调用非const函数,不要返回对数据成员的非const指针或引用;
  • 如果数据成员在对象构造之后不再改变,可将其定义为const

允许使用mutable关键字,但是在多线程中它并不安全,因此首先要仔细考虑线程安全。

const的位置

有人喜欢int const *foo形式,不喜欢const int* foo,他们认为前者更加一致因此可读性更好:它遵循了const总位于其描述的对象(int)之后的原则。但是,一致性原则并不适用于浅嵌套的指针表达式,因为大部分const表达式中只有一个const来修饰潜在的值,这种情况下,不需要维护一致性。将const放在前面才更易读,因为在自然语言中形容词(const)是在名词(int)之前的。

也就是说,虽然我们鼓励将const放在前面,但这不是必须的。重要的是保持代码风格一致。

constexpr用法

在C++11中,用constexpr来定义真正的常量或者保证常量的初始化。

定义:通过constexpr来指示变量是真正的常量,例如,在编译、链接阶段是固定不变的。将函数或者构造函数定义为constexpr使得他们可以用来定义一个constexpr变量。

优点:通过constexrp,可以定义浮点表达式的常量,而不是只能定义直接常量;可以定义自定义类型的常量;可以通过函数调用定义常量。

缺点:过早的标记constexpr可能会导致迁移问题,如果后续被迫降级的话。当前对constexpr函数和构造函数的限制可能会导致他们的定义含糊不清。

结论

constexpr使得接口的常量部分的鲁棒性更好;用constexpr来指定真正的常量以及支持这些定义的函数;不要为了使用constexpr而把函数定义复杂化;不要用constexpr来强制内联。

Integer类型

C++内建整型中,唯一用到的是int,如果程序中需要不同大小的变量,可以使用<stdint.h>中的精确宽度(precise-width)的整型,如int16_t。如果变量值可能大于等于2^31(2GiB),使用64位类型,如int64_t。注意即使变量值不会大于int的最大值,中间计算过程的值可能大于int最大值。如果有疑问,那么就选择位宽更大的类型。

定义:C++没有指定整型的大小,通常人们认为short是16位,int是32位,long是32位,long long是64位。

优点:保持声明一致性。

缺点:C++整形的大小取决于编译器和平台架构。

结论:

<stdint.h>定义了int16_tuint32_tint64_t等整型,在需要确定大小的整型时可以使用它们代替shortunsigned long long等.在C整型中,只使用int。适当情况下,推荐使用标准类型如size_tptrdiff_t

最常使用的是int,因为对整数来说,通常不会用到太大,如循环计数等,可以使用普通的int。你可以认为int至少为32位,但不要认为它会多于32位,需要64位整型的话,可以使用int64_tuint64_t

对于大整数,使用int64_t

不要使用uint32_t等无符号整型,除非你是在表示一个位组(bit pattern)而不是一个数值,否则你要对2^N取余。需要特别指出,不要用无符号类型来表示值不可能为负,使用断言做保护。

如果是返回大小的容器,那么务必使用一个能满足容器中任何可能用法的类型。如果不确定,选用较大的类型。

转换整数类型时需格外小心。整数类型转换和提升会产生隐蔽的行为。

Unsigned Integers类型

有些人,包括一些教科书作者,推荐使用无符号类型表示非负数,类型表明了数值取值形式。但是,在C语言中,这一优点被由其导致的bugs所淹没。看以下代码:

1
2
// buggy code
for (unsigned int i = foo.Length()-1; i >= 0; --i) ...

上述代码永远不会终止!有时gcc会发现该bug并报警,但通常不会。类似的bug还会出现在比较有符号变量和无符号变量时,主要是C的类型提升机制(type-promotion scheme,C语言中各种内建类型之间的提升转换关系)会致使无符号类型的行为出乎你的意料。

因此,使用断言保证变量非负,不要使用无符号型。

64位移植性

代码在64位和32位的系统中,原则上应该都是友好的,尤其对于输出、比较、结构对齐(structure alignment)来说:

  • printf() specifiers for some types are not cleanly portable between 32-bit and 64-bit systems. C99 defines some portable format specifiers. Unfortunately, MSVC 7.1 does not understand some of these specifiers and the standard is missing a few, so we have to define our own ugly versions in some cases (in the style of the standard include file inttypes.h):
  • printf()指定的一些类型在32位和64位系统上可移植性不是很好,C99标准定义了一些可移植的格式。不幸的是,MSVC 7.1并非全部支持,而且标准中也有所遗漏。所以有时我们就不得不自定义丑陋的版本(使用标准风格要包含文件inttypes.h):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// printf macros for size_t, in the style of inttypes.h
#ifdef _LP64
#define __PRIS_PREFIX "z"
#else
#define __PRIS_PREFIX
#endif
 
// Use these macros after a % in a printf format string
// to get correct 32/64 bit behavior, like this:
// size_t size = records.size();
// printf("%"PRIuS"\n", size);
 
#define PRIdS __PRIS_PREFIX "d"
#define PRIxS __PRIS_PREFIX "x"
#define PRIuS __PRIS_PREFIX "u"
#define PRIXS __PRIS_PREFIX "X"
#define PRIoS __PRIS_PREFIX "o"
类型不要使用类型使用类型备注
`void *`或任何指针 `%lx` `%p`  
`int64_t` `%qd,%lld` `%"PRId64"`  
`uint64_t` `%qu, %llu, %llx` `%"PRIu64", %"PRIx64"`  
`size_t` `%u` `%"PRIuS", %"PRIxS` C66指定`%zu`
`ptrdiff_t` `%d` `%"PRIdS"` C99指定`%td`

注意宏PRI*会被编译器扩展为独立字符串,因此如果使用非常量的格式化字符串,需要将宏的值而不是宏名插入格式中,在使用宏PRI*时同样可以在%后指定长度等信息。例如,printf("x = %30"PRIuS"\n", x)在32位Linux上将被扩展为printf("x = %30" "u" "\n", x),编译器会将之处理为printf("x = %30u\n", x)

  • 记住sizeof(void *) != sizeof(int),如果需要一个表示指针大小的整数要使用intptr_t
  • 需要对结构对齐加以留心,尤其是对于存储在磁盘上的结构体。在64位系统中,任何拥有int64_t/uint64_t成员的类/结构体将默认被处理为8字节对齐。如果32位和64位代码共用磁盘上的结构体,需要确保两种平台架构下的结构体的对齐一致。大多数编译器提供了调整结构体对齐的方案。gcc中可使用__attribute__((packed)),MSVC提供了#pragma pack()__declspec(align())(注,解决方案的项目属性里也可以直接设置)。
  • 创建64位常量时使用LLULL作为后缀,如:
1
2
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;
  • 如果你确实需要32位和64位系统具有不同的代码,用#ifdef _LP4来隔离这些代码。(尽量不要这么做,使用时尽量使修改局部化)。

预处理宏

使用宏时要谨慎,尽量以内联函数、枚举类型和常量代替宏。

宏意味着你和编译器看到的代码是不同的,因此可能导致异常行为,尤其是当宏存在于全局作用域中。

幸运的是,C++中,宏不像C中那么必要。可以用内联函数来代替宏内联的效率关键代码(performance-critical code);宏存储常量可以const变量替代;宏“缩写”长变量名可以引用替代;使用宏进行条件编译,这个……,最好不要这么做,会令测试更加痛苦(#define防止头文件重包含当然是个例外)。

宏可以做一些其他技术无法实现的事情,在一些代码库(尤其是底层库中)可以看到宏的某些特性(如字符串化(stringifying,译者注,使用#)、连接(concatenation,译者注,使用##)等等)。但在使用前,仔细考虑一下能不能不使用宏实现同样效果。

下面给出可以避免一些宏使用问题的方法,以供参考:

  • 不要在.h文件中定义宏;
  • 使用前才#define,使用后立即#undef
  • 不要只是在替换前才对已经存在的宏使用#undef,而是要选择一个不会冲突的名称;
  • 不使用会导致不稳定的C++构造(unbalanced C++ constructs)的宏,否则至少需要文档说明其功能正确;
  • 不推荐使用##来生成函数、类、变量名。

0和nullptr/NULL

整数用0,实数用0.0,这一点毫无争议。

指针用nullptrNULL,字符(串)用'\0'

至于指针(地址值),根据实际选择用0NULL还是nullptr。对使用了C++11特性的项目,选用nullptr;对于C++03项目,推荐NULL,因为它像是一个指针。实际上,有些C++编译器特别定义了NULL,使得这些编译器能够给出游泳的告警,特别是当sizeof(NULL)不等于sizeof(0)的时候。

字符(串)用'\0',不仅类型正确而且可读性好。

sizeof

尽可能用sizeof(varname)来代替sizeof(type)

在需要获取变量大小时使用sizeof(varname),这样当变量类型改发时代码自动同步;当不与任何变量有关系时可以用sizeof(type),比如在管理内部或者外部的数据格式但是不便使用C++类型的变量时。

1
2
3
4
5
6
7
8
9
Struct data;
memset(&data, 0, sizeof(data)); // GOOD
 
memset(&data, 0, sizeof(Struct)); // BAD
 
if (raw_size < sizeof(int)) {
    LOG(ERROR) << "compressed record not big enough for count: " << raw_size;
    return false;
}

auto

使用auto来代替手敲那些乱哄哄的名字;当有利于可读性时使用明白的类型声明;除了局部变量,不要使用auto

定义:在C++11中,冠以auto的变量会在初始化时被赋予和初始化表达式相匹配的类型。既可以用auto来通过拷贝初始化变量,也可以绑定到某个引用上。

1
2
3
4
vector<string> v;
...
auto s1 = v[0];  // Makes a copy of v[0].
const auto& s2 = v[0];  // s2 is a reference to v[0].

优点:

有时C++的类型名又长又臭,特别是引入模版或者命名空间后。例如:

1
sparse_hash_map<string, int>::iterator iter = m.find(val);

返回的类型阅读体验极差,这段代码的主要目的也很晦涩。做如下修改,读起来就舒服多了:

1
auto iter = m.find(val);

没有auto的话,有时我们不得不在一个表达式里面写两次类型名,对读者来说毫无意义。例如:

1
diagnostics::ErrorStatus* status = new diagnostics::ErrorStatus("xyz");

适当的使用auto,减少了书写类型的负担,使得中间变量的使用变得轻而易举。

缺点:

有时明确类型的代码很清晰,尤其是变量初始化依赖在其它地方的代码时。譬如下面的表达式:

1
auto i = x.Lookup(key);

i的类型就不明显,如果x在数百行之前的代码里面声明的话。

程序员必须理解autoconst auto&的不同之处,否则将事与愿违。

auto和C++11大括号初始化的交互可能让人迷惑。声明如下:

1
2
auto x(3);  // Note: parentheses.
auto y{3};  // Note: curly braces.

他们代表不同的事情:xint,而y是初始化列表。这同样适用于其它不常见的代理类型。

如果auto变量是接口的一部分,譬如,头文件中的一个常量,程序员可能在意图改变其值时却改变了其类型,从而导致意外的API修改。

结论:

仅允许局部变量使用auto;对于文件作用域或者命名空间内的变量或类成员,禁止用auto;禁止将大括号初始化列表赋值给auto变量。

auto在非C++11特性相关的应用:作为一种新的函数声明语法的一部分,这种函数尾随返回的类型。但是,禁止使用该语法。

大括号初始化

允许使用大括化初始化(brace initialization)。

在C++03中,聚合类型(数组和没有构造函数的结构体)可以通过大括化初始化。

1
2
struct Point { int x; int y; };
Point p = {1, 2};

在C++11中,所有的数据类型都可以使用该语法。大括号初始化形式被称为大括号初始化列表(braced-init-list)。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Vector takes lists of elements.
vector<string> v{"foo""bar"};
 
// The same, except this form cannot be used if the initializer_list
// constructor is explicit. You may choose to use either form.
vector<string> v = {"foo""bar"};
 
// Maps take lists of pairs. Nested braced-init-lists work.
map<int, string> m = {{1, "one"}, {2, "2"}};
 
// braced-init-lists can be implicitly converted to return types.
vector<int> testFunction() {
  return {1, 2, 3};
}
 
// Iterate over a braced-init-list.
for (int i : {-1, -2, -3}) {}
 
// Call a function using a braced-init-list.
void testFunction2(vector<int> v) {}
testFunction2({1, 2, 3});

自定义数据类型也可以定义使用initializer_list(通过大括号初始化列表可以自动创建)的构造函数。

1
2
3
4
5
6
7
8
9
10
class MyType
{
public:
    // initializer_list is a reference to the underlying init list,
    // so it can be passed by value.
    MyType(initializer_list<int> init_list) {
      for (int element : init_list) {}
    }
};
MyType m{2, 3, 5, 7};

大括号初始化也可以调用不含initializer_list的普通构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
double d{1.23};
// Calls ordinary constructor as long as MyOtherType has no
// initializer_list constructor.
class MyOtherType
{
public:
    explicit MyOtherType(string);
    MyOtherType(int, string);
};
MyOtherType m = {1, "b"};
// If the constructor is explicit, you can't use the "= {}" form.
MyOtherType m{"b"};

禁止将大括号初始化列表赋值给auto变量,否则可能会引起歧义。

1
2
3
auto d = {1.23};        // d is an initializer_list<double>
 
auto d = double{1.23};  // Good -- d is a double, not an initializer_list.

Lambda表达式

仅在类似cocos2d回调这样的特殊地方使用lambda表达式、std::function或者std::bind

定义:Lambda表达式可以很简洁的创建匿名函数对象,当函数作为参数时很有用。例如,std::sort(v.begin(), v.end(), [](http://cn.cocos2d-x.org/article/index?type=wiki&url=/doc/cocos-docs-master/manual/framework/native/wiki/cpp-coding-style/string x, string y) { return x[1] < y[1]; });。和Lambda表达式一起被引入到C++11中的还包括一系列的函数对象特性,如多态封装std::function

优点:

  • Lambda表达式可以定义更简洁的传递给STL算法的函数对象,这提高了代码可读性。
  • Lambda表达式、std::function以及std::bind可以组合起来作为回调机制;因此可以轻松编写函数作为参数的函数。

缺点:

  • 在Lambda表达式中捕获变量是件棘手的事情,并可以导致悬垂指针(dangling-pointer)。
  • lambda表达式的滥用:理解超长嵌套的匿名函数非常痛苦。

结论:仅在类似cocos2d回调这样的特殊地方使用lambda表达式、std::function或者std::bind

Boost

禁止使用boost

定义:Boost库集是一个非常流行、经过同行评审、免费、开源的C++库。

优点:Boost代码质量普遍较高、可移植性好,填补了C++标准库多项空白,如型别特性(type traits)、更完善的绑定(binders)、更好的智能指针,同时提供了TR1(标准库的扩展)的实现。

Cons:某些Boost库提倡的编程实践可读性差,像元编程(metaprogramming)和其他高级模板技术,以及过度“函数化”("functional")的编程风格。也导致cocos2d-x需要的依赖更多。

结论:为使得代码对贡献者有较高的可读性,以及尽可能减少依赖,禁止使用Boost。

C++11

适当的时候使用C++11(原名为C++0x)的扩展库和语言。使用前需要考虑可移植性。

定义:C++11时最新的ISO C++ 标准。包含语言和库的重大修改。

优点:C++11已成为官方标准,并且实际上被大部分的C++编译器支持。已被使用的一些C++扩展成为标准,允许简写某些操作,并提升了性能和安全性。

缺点:

C++11标准比上一任标准复杂的多(1300页VS800页),对很多开发者来说很陌生。某些特性在代码可读性和可维护性上的长期影响不得而知。无法预测它的诸多特性在各种工具上能否得到统一的实现,特别是当项目必须使用老版本的工具时。

和Boost一样,一些C++11扩展鼓励降低代码可读性的编程实践-例如,删除对读者有价值的冗余检查(如类型名),或者鼓励模版元编程。其它扩展重复了现有机制已经实现的功能,可能导致混乱和转换开销。

结论:

如无规定,可以使用C++11特性。除了本文剩余部分描述的内容,不要使用下列C++11特性:

  • 带尾返回类型的函数,例如,auto foo() -> int;,因为要与以后的函数声明保持风格一致。
  • 编译时的有理数(<ratio>),因为担心其依赖于一个重模版(template-heavy)接口风格。
  • <cfenv><fenv.h>头。因为很多编译器还不支持这些特性。

额外的,任何使用的C++11特性必须能在以下编译器上工作:

  • Xcode 5.0.2或更高
  • gcc 4.8或更高
  • VS 2012或更高

一般命名规则

函数名、变量名、文件名应是描述性的,避免缩写。

尽可能使用描述性名称,不要节约行空间,让别人快速理解你的代码更重要。不要使用过度的缩写或者项目之外的读者不熟悉的缩写,不要用省略字母的缩写。

1
2
3
4
// OK
int priceCountReader;     // No abbreviation.
int numErrors;            // "num" is a widespread convention.
int numDNSConnections;    // Most people know what "DNS" stands for.
1
2
3
4
5
6
7
// BAD
int n;                     // Meaningless.
int nerr;                  // Ambiguous abbreviation.
int nCompConns;            // Ambiguous abbreviation.
int wgcConnections;        // Only your group knows what this stands for.
int pcReader;              // Lots of things can be abbreviated "pc".
int cstmrId;               // Deletes internal letters.

文件名

文件名必须遵循驼峰命名,对cocos2d项目的文件,应该已CC开头。

好的命名示例:

1
2
3
CCSprite.cpp
CCTextureCache.cpp
CCTexture2D.cpp

C++文件以.cpp结尾,头文件以.h结尾。

禁止使用/usr/include目录下已存在的文件名,如db.h

通常,尽量让文件名更加明确,譬如使用CCTexture2D.h,而不是Texture.h。最常见的情况是文件成对命名,如FooBar.hFooBar.cpp,定义类为FooBar

内联函数必须放在.h文件中,如果内联函数比较短,就直接放在.h中。如果代码比较长,可以放到以-inl.h结尾的文件中。对于包含大量内联代码的类,类应该有三个文件:

1
2
3
UrlTable.h       // The class declaration.
UrlTable.cpp     // The class definition.
UrlTable-inl.h   // Inline functions that include lots of code.

亦可参考inl.h文件一章。

类型名

类型名每个单词以大写字母开头,不包含下划线:MyExcitingClassMyExcitingEnum

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

1
2
3
4
5
6
7
8
9
10
// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
 
// typedefs
typedef hash_map<UrlTableProperties *, string> PropertiesMap;
 
// enums
enum UrlTableErrors { ...

变量名

变量名必须遵循驼峰命名发(首字母小写)。类成员变量以下划线(_)作为前缀。例如:myExcitingLocalVariable ,_myExcitingLocalVariable

也接受全小写的变量。

普通变量名

示例:

1
2
3
4
5
string tableName;  // OK - uses camelcase
string tablename;   // OK - all lowercase.
 
string table_name;   // Bad - uses underscore.
string TableNname;   // Bad - starts with Uppercase

类数据成员

数据成员(亦称为实例变量或者成员变量)和普通的变量名一样是小写并且可选下划线,但是数据成员总是以下划线作为前缀。

1
2
string _tableName;   // OK
string _tablename;   // OK

结构体变量

结构体数据成员像普通变量那样命名,但是不带下划线前缀。

1
2
3
4
struct UrlTableProperties {
  string name;
  int numEntries;
}

参考结构 vs 类一章关于何时用结构体何时用类的讨论。

全局变量

对全局变量没有特别要求,少用就好,可以以g_或其他易与局部变量区分的标志为前缀。

常量名

常量名应该全部大写并用下划线分隔不同的词。

常量不要使用关键字#define

尽量使用强类型枚举常量,只要这样做可行。

所有编译时常量,它们是否声明在局部或全局范围内,或作为一个类的一部分,遵循一个略微不同的来自其他变量的命名约定。他们应该用大写字母中声明和使用下划线分隔不同的词:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const int MENU_DEFAULT_VALUE = 10;
const float GRAVITY = -9.8;
 
enum class Projection {
  ORTHOGONAL,
  PERSPECTIVE
};
 
enum class PixelFormat {
  RGBA_8888,
  RGBA_4444,
  RGBA_5551,
  RGB_565,
};

函数名称

通常函数名有大小写混合的;访问器和存取器相匹配的变量的名称: myExcitingFunction()myExcitingMethod(),getMyExcitingMemberVariable()setMyExcitingMemberVariable

一般函数

函数应该以小写开始,并且后面每个新的单词的首字母都要大写。没有下划线。

如果你的函数因为一个错误而崩溃,你应该将OrDie添加到函数名。这只适用于函数可以用合理的产生代码和一些可能发生在正常操作的错误。

1
2
3
addTableEntry()
deleteUrl()
openFileOrDie()

访问器和存储器

访问器和存储器 ( get 和 set 函数) 应该匹配他们要设置或获取的变量的名字。这个展示了一个类的片段,它的实例变量是_numEntries

1
2
3
4
5
6
7
8
9
class MyClass {
 public:
  ...
  int getNumEntries() const return _numEntries; }
  void setNumEntries(int numEntries) { _numEntries = numEntries; }
 
 private:
  int _numEntries;
};

命名空间的名称

命名空间的名称全部为小写,并且是基于项目名称或目录结构:google_awesome_project。

看命名空间关于命名空间的讨论和如何命名。

枚举器名称

枚举器的命名应该像常量:ENUM_NAME

Prefer strongly typed enums over non-strongly typed enums.

1
2
3
4
5
enum class UrlTableErrors {
  OK = 0,
  ERROR_OUT_OF_MEMORY,
  ERROR_MALFORMED_INPUT,
};

宏命名

你是不是真的要定义一个宏,是吗? 如果是, 他们应该像这样: CC_MY_MACRO_THAT_SCARES_SMALL_CHILDREN。

请看宏的描述:通常情况下,宏不应该被使用,但是如果一定需要的话, 那么所有的字符应该大写并带有下划线, 而且要有CC_CC前缀。

1
2
3
#define CC_ROUND(x) ...
#define CC_PI_ROUNDED 3.0
#define CCLOG(x) ...

异常的命名规则

如果你命名的东西,类似于现有的C或C++的实体,那么你可以使用哪个现有的命名规范方案。

bigopen()

函数名, 符合open()形式

uint

类型定义

bigpos

结构体或类, 符合pos形式

sparse_hash_map

类似STL实体; 符合STL命名规范

LONGLONG_MAX

一个常量, 在INT_MAX中

注释

虽然写起来很痛苦,但是注释对于保持代码可读性绝对非常重要。接下来的规则描述了哪些代码需要注释以及在什么地方添加注释。请记住:注释固然非常重要,但是好的代码就是自己的文档。为类型和变量取有意义的名字比晦涩的名字加注释要好的多。

为你的读者,也就是下一个需要读懂你的代码的贡献者写注释。不要吝啬-下一个可能就是你!

Doxygen

  • 在头文件中添加Doxygen注释。在实现文件中不需要写Doxygen注释。
  • 所有的pulic class的对象必须有Doxygen注释解释它的功能。
  • 所有类的public方法,除了重载的方法,必须有Doxygen注释。
  • protectedprivate方法建议添加Doxygen注释,但不是强制的。
  • 实例变量需要Doxygen注释,除非他们是public的。
  • 可以在Doxygen注释中使用Markdown语法。

Example:

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
26
27
28
29
30
31
32
33
34
35
/** `WorldPeace` extends `Node` by adding enough power to create world peace.
 *
 * `WorldPeace` should be used only when the world is about to collapse.
 * Do not create an instance of `WorldPeace` if the `Scene` has a peace level of 5.
 *
 */
class WorldPeace : public Node
{
 
public:
    /** creates a `WorldPeace` with a predefined number of preachers
     */
    static WorldPeace* create(int numberOfPreachers);
 
    /** sets the number of preachers that will try to create the world peace.
    The more the better. But be aware that corruption might appear if the number if higher than the 20% of the population.
    */
    void setNumberOfPreachers(int numberOfPreachers);
 
    /** displays an aura around the WorldPeace object
    @see See all `displaySuperAura()`
    */
    void displayAura();
 
    // Overrides
    virtual void addChild(Node * child) override;
    virtual void removeChild(Node* child, bool cleanup) override;
 
protected:
    WorldPeace();
    virtual ~WorldPeace();
    bool init(int nubmerOfPreachers);
 
    int _nubmerOfPreachers;
};

注释规范

使用//或者/* */,但是需要保持一致。

你可以使用//或者/* */,不过//更常用。与你如何注释以及使用的风格保持一致。

文件注释

文件以许可条文开始,紧接着是许可内容描述。

法律声明和作者

文件应该包含许可条文。为项目使用的许可选择适用的条文(例如Apache 2.0、BSD、MIT等等)。

许可必须兼容不同的应用商店,所以cocos2d-x不能使用GPL和LGPL。

如果文件的某个作者发生了重大改变,那么考虑删除该作者这一行。

文件内容

文件要在开始的地方添加注释描述文件的内容。

通常.h文件会概述它所声明的类的功能以及使用方法。.cpp文件会包含实现细节或者复杂算法的更多信息。如果你认为实现细节或者复杂算法的讨论有助于别人阅读.h文件,那就放到.h文件中,但是要在.cpp文件中注明文档在.h文件中。

不要在.h和.cpp中重复注释。重复注释会产生歧义。

类注释

类定义应该附带着说明类的功能和使用方法的注释。如果类是public(暴露给用户),那么应该有Doxygen注释。

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

如果在文件顶部已经有详细描述一个类的注释,那么可以通过"See comment at top of file for a complete description"简单的说明,但是一定要确保有类似的注释。

如果类有同步假设的话,记录这些劫杀。如果某个实例可以在多线程里访问,那么请倍加小心的记录多线程使用的规则和常量(invariants)。

函数注释

声明函数的时候注释函数的用法;定义函数的时候注释函数的操作。

如果函数是public(暴露给用户),用Doxygen注释。

函数声明

声明函数前,需要添加描述函数功能和调用方法的注释。注释应该是描述式的("Opens the file"),而不是命令式的("Open the file");注释描述函数功能,但是不描述函数实现。通常,函数声明的注释不描述函数如何执行任务。这类的注释应该放在函数定义的地方。

函数声明的注释需要包含以下内容:

  • 输入、输出
  • 对于类成员函数:对象是否超出方法调用周期保存引用参数,以及是否释放引用参数。
  • 函数是否分配了需要调用者释放的内存。
  • 是否有参数可以是NULL指针。
  • 是否有影响函数性能的行为
  • 可重入函数的同步假设是什么

示例:

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;

但是,对于易懂的代码,不要产生不必要的冗余描述。注意到下面的例子中,不需要"returns false otherwise",因为它已经隐含在注释中。

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

为构造函数和析构函数写注释时,请记住读者知道构造函数和析构函数的用途,所以类似“destorys this object”的注释没有意义。记录构造函数对参数的处理(例如,是否拥有指针),析构函数的释放行为。如果很琐碎,可以不用注释。析构函数没有注释是很普遍的事。

函数定义

如果函数很复杂,那么函数定义应该有说明性的注释。例如,在函数定义的注释中,可以描述使用的复杂代码,概述步骤,或者解释为什么采用这种方法来实现函数,而不是用其他可行的方案。例如,你可能提及为什么在函数的前半段必须获取锁但是在后半段不需要。

注意,不能只是单纯重复.h文件或者其他地方里函数声明时的注释。简要得复述函数的用途是可以的,但是注释应该聚焦在如何实现上。

变量注释

一般情况下,变量的名字应该要很好得体现变量的用途。在某些情况下,需要更多的注释。

类成员

类成员(亦称为实例变量或成员变量)需要注释描述它的用途。如果变量可以是有特殊意义的定值,例如NULL指针或者-1,也要注释。例子如下:

1
2
3
4
5
private:
 // Keeps track of the total number of entries in the table.
 // Used to ensure we do not go over the limit. -1 means
 // that we don't yet know how many entries the table has.
 int _numTotalEntries;

全局变量

和数据成员一样,所有的全局变量都要有注释来描述变量是什么以及用途。例如:

1
2
// The total number of tests cases that we run through in this regression test.
const int NUM_TEST_CASES = 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;
}

单行注释

同样的,不易理解的单行应该在行尾添加注释。注释和代码之间用2个空格隔开。例如:

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
doSomething();                  // Comment here so the comments line up.
doSomethingElseThatIsLonger();  // Comment here so there are 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.
}
doSomething(); /* For trailing block comments, one space is fine. */

nullptr/NULL, true/false, 1, 2, 3...

当向函数传递空指针、布尔值或者整数值时,建议添加注释说明,或者使用常量。例如:

不好的代码:

1
2
3
4
bool success = calculateSomething(interesting_value,
                                  10,
                                  false,
                                  NULL);  // What are these arguments??

好的代码:

1
2
3
4
bool success = calculateSomething(interesting_value,
                                  10,     // Default base value.
                                  false,  // Not the first time we're calling this.
                                  NULL);  // No callback.

另一种方案,使用常量或者名字有意义的变量:

1
2
3
4
5
6
7
const int DEFAULT_BASE_VALUE = 10;
const bool FIRST_TIME_CALLING = false;
Callback *nullCallback = NULL;
bool success = CalculateSomething(interestingValue,
                                  DEFAULT_BASE_VALUE,
                                  FIRST_TIME_CALLING,
                                  nullCallback);

Don'ts

注意,永远不要描述代码本身。阅读代码的人的C++可能比你好,即使他们还不知道你要做的事情:

1
2
3
// Now go through the b array and make sure that if i occurs,
// the next element is i+1.
...        // Geez.  What a useless comment.

标点,拼写和语法

留意标点、拼写和语法;写的好的注释比坏的注释更容易阅读。

注释应该是大小写、标点使用正确的叙述性的文字。很多情况下,完整的句子优于只言片语。简短的注释,如单行注释,有时不那么正式,但是让风格保持一致。

尽管代码评审时被指出应该用分号(;)而不是逗号(,)令人沮丧,但是源代码保持高度清晰和可读是非常重要的事情。正确的标点、拼写以及语法有助于达到这一目标。

TODO注释

为临时的或者短期解决方案或者很好但不完美的代码添加TODO注释。

TODO注释要包含全大写的TODO字符,紧跟着是最适合完成TODO事项的人的姓名、E-mail地址或者其他任何身份。这样的主要目的是保持一致的TODO注释格式,可以根据需求查到可以提供更多信息的人。TODO不承诺提及的人将会解决这个问题。所以,当你创建一个TODO时,几乎总是写自己的名字。

1
2
// TODO(kl`gmail.com): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.

如果TODO是“未来某天做某事”类型,确保要么包含一个确切的日期("Fix by November 2005"),要么包含一个确切的事件("Remove this code when all clients can hanle XML responses")。

弃用注释

用宏CC_DEPRECATED_ATTRIBUTE来将方法标记为弃用。

同时在注释中用``deprecated来将其标记为弃用。

弃用注释必须包含简单、清晰的说明来帮助用户修改调用点。在C++中,你可以通过在內联函数中调用新的接口来实现弃用函数。

标记一个接口为弃用并不会导致调用点发生变化。如果你希望别人真正的停止使用这些弃用的函数,你得自己或者找人帮你修改这些调用点。

新的代码不能调用已弃用的接口,使用新的接口来作为替代。如果你无法理解说明,向创建废弃的人咨询如何使用新的接口。

格式化

编码风格和格式是相当随意的,但如果每个人都使用相同的样式一个项目是很容易遵循这套风格的。个人可能不统一格式规则各个方面规则,有些规则可能需要一些时间来适应,但重要的是,所有的项目贡献者遵循的同样的规则,使他们都可以阅读和轻松地了解每个人的代码。

要正确地帮你格式化代码,我们为emacs创建了一个配置文件。

行长度

你的代码中每行不能超过80个字符长度。

我们意识到,这条规则是有争议的,但这么多的现有代码已经坚持它,并且我们觉得这种一致性是很重要的。

优点: 那些赞成这个规则的人认为,迫使他们改变自己的习惯这是不礼貌的,也没有必要对任何事都如此。有些人习惯把几个代码窗口并排侧,因此无论怎样都没有足够的空间扩阔他们的代码窗。人们建立自己的工作环境假设一个非常最大的窗口宽度并且80列一直是传统的标准。为什么要改变呢?

缺点: 变革的支持者认为,一个更宽的行可以使代码更具可读性。80列的限制是一个墨守成规倒退到20世纪60年代的大型机;现代化的设备具有很广的屏幕,可以很容易地显示出更长的行。

决策:

80个字符长度是上线。

例外: 如果一个注释行包含一个示例命令或文字的URL长度超过80个字符,该行可能会超过80个字符以便于剪切和粘贴。

例外: 一个有很长路径的#include语句可能会超过80列。 尽量避免这种情况成为必要。

例外: 你不必关心超过最大长度的报头警卫。

非ASCII字符

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

哪怕是英文,也丌应将用户界面的文本硬编码到源代码中,因此非ASCII字符要少用。特殊情冴下可以适当包吨此类字符。如,代码分析外部数据文件时,可以适当硬编码数据文件中作为分隔符的非ASCII字符串; 更常用的是(不需要本地化的)单元测试代码可能包含非ASCII字符串。此类情况下,应使用UTF-8格式,因为很多工具都可以理解和处理其编码,十六进制编码也可以,尤其是在增强可读性的情况下——如"\xEF\xBB\xBF"是Unicode的zero-width no-break space字符,以UTF-8格式包含在源文件中是不可见的。

使用U8前缀,以保证一个字符串包含\uXXXX转义序列编码为UTF-8。不要使用它含有编码为UTF-8非ASCII字符的字符串,因为这会产生不正确的输出,如果编译器不解释源文件为UTF-8。

你不应该使用C++11的char16_tchar32_t字符类型,因为它们是用于非UTF-8的文本。出于类似的原因,你也不要使用wchar_t(除非你正在编写与Windows交互的API,它广泛使用wchar_t)。

空格还是制表位

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

使用空格进行缩进,不要在代码中使用tabs,设定编辑器将tab转为空格。

函数声明与定义

返回类型和函数名在同一行,合适的话,参数也放在同一行。

函数看上去像这样:

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

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

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

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

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

注意以下几点:

  • 如果你无法使的返回类型和函数名​​在同一行中,分开它们。
  • 如果一个函数定义的返回类型后停止,不缩进。
  • 左括号总是和函数名在同一行。
  • 函数名和左括号之间永远不要有空格。
  • 括号和参数之间永远不要有空格。
  • 左大括号总是在同一行的最后一个参数的结尾。
  • 结束大括号或者是由本身的最后一行或(如果其他样式规则允许)在同一行的大括号。
  • 右括号和大括号之间应该有一个空格。
  • 所有的参数应该被命名,在该声明和实现相同的名称。
  • 所有参数应尽可能保持一致。
  • 默认缩进4个空格。
  • 包装参数有4个空格缩进。

如果某些参数是未使用的,在函数定义中注释掉变量名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 接口中总是有一个命名了的参数。
class Shape
{
public:
    virtual void rotate(double radians) = 0;
}
 
// 声明中总是有一个命名了的参数。
class Circle : public Shape
{
public:
    virtual void rotate(double radians);
}
 
// 注释掉定义中未使用的命名参数。
void Circle::rotate(double /*radians*/)
{}
 
// 不好的做法。如果有人想之后再实现,那么变量的意义目前还不清楚是什么。
 
void Circle::rotate(double)
{}

函数调用

在同一行,如果它适合;否则,包裹在括号中的参数。

函数调用有以下格式:

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

如果参数没有在同一行的全部适合的,他们应该被分解到多行,每个后续行与第一个参数一致。左括号后或右括号前不要加空格:

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

如果函数有很多参数,考虑每行一个,如果这使得代码更易读:

1
2
3
4
bool retval = doSomething(argument1,
                          argument2,
                          argument3,
                          argument4);

所有参数可以有选择的放在随后的行上,每一行一个参数:

1
2
3
4
5
6
7
8
9
10
if (...) {
    ...
    ...
    if (...) {
        doSomething(
            argument1,  // 4 space indent
            argument2,
            argument3,
            argument4);
    }

特别注意的是,如果函数签名很长,所以它不能在行最大长度内适应。

大括号初始化列表

格式化大括号列表就像你会格式化在其位置的函数调用。

如果大括号列表后面跟一个名称(例如一个类型或变量名),格式如{}都具有该名称的函数调用。如果没有名字,假设一个长度为零的名字。

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<intint> 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 = {  // Here, you could also break before {.
    superlongvariablename1,
    superlongvariablename2,
    {short, interior, list},
    {interiorwrappinglist,
     interiorwrappinglist2}};

条件语句

宁愿括号内不能有空格。其他的关键字属于新的一行。

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

最常见的形式是没有空格。要么是好的,但要保持一致。如果要修改一个文件,使用已经存在的格式。如果您正在编写新的代码,使用该目录或项目使用的其他文件的格式。如果有疑问,在没有偏好的情况下,不加空格。

1
2
3
4
5
6
if (condition) {  // 括号内没有空格
    ...  // 4 空格缩进
else if (...) {  // else在同一行的右大括号旁边
else {
    ...
}

如果喜欢可以在括号内加空格

1
2
3
4
5
if ( condition ) {  // 括号内加空格 少用
    ...  // 4 空格缩进
else {  // else在同一行的右大括号旁边
    ...
}

请注意,在所有情况下,if和左括号之间你必须有空格。右括号和大括号之间必须有空格,如果你使用一个。

1
2
3
4
5
if(condition)     // 不好 - IF后面没有空格.
if (condition){   // 不好 - {前面没有空格
if(condition){    // 不好
 
if (condition) {  // 好 - IF后和{前都有空格

简短的条件语句可以写在一行,如果这增强了可读性。只有当行非常短而且语句不使用else子句时你可以使用这个。

1
2
if (x == FOO) return new Foo();
if (x == BAR) return new Bar();

当if语句有一个else是不允许的:

1
2
3
// 不允许 - 有一个ELSE子句时IF语句在同一行
if (x) doThis();
else doThat();

一般情况下,是不需要单行语句大括号,但他们如果你喜欢他们被允许;复杂条件或条件语句或循环语句可能更容易阅读使用花括号。有些项目需要的,如果必须始终始终有一个附带的支架。

1
2
3
4
5
6
if (condition)
    doSomething();  // 缩进4个空格
 
if (condition) {
    doSomething();  // 缩进4个空格
}

但是,如果一个if-else语句的一部分使用花括号,另一部分也一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 不允许 - IF中有花括号但ELSE中没有
if (condition) {
    foo;
else
    bar;
 
// 不允许 - ELSE中有花括号但IF中没有
if (condition)
    foo;
else {
    bar;
}
 
// 大括号围绕两个IF和ELSE是必需的,因为子句之一使用了大括号。
if (condition) {
    foo;
else {
    bar;
}

循环和选择语句

switch语句可以使用大括号块。在情况间注释特别的失败。空循环体应使用{}或continue。

在switch语句中case块可以有大括号与否,取决于您的喜好。如果你这样做,包括大括号他们应放在如下所示。

如果枚举值不是有条件的,switch语句应该始终有一个默认的情况下(在枚举值的情况下,编译器会警告你,如果不处理任何值)。如果默认情况下不应该执行,简单地断言:

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

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

1
2
3
4
5
6
7
while (condition) {
    // 反复测试直到返回false.
}
for (int i = 0; i < SOME_NUMBER; ++i) {}  // 很好 - 内容为空.
while (condition) continue;  // 很好 - continue暗示没有逻辑实现
 
while (condition);  // 不好 - 看起来像do/while循环的一部分

指针和引用表达式

No spaces around period or arrow. Pointer operators do not have trailing spaces.没有空格围绕句号或箭头。指针运算符没有空格结尾。

以下是格式正确的指针和引用表达式的例子:

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

需要注意的是:

  • 访问成员时句号或箭头周围没有空格。
  • 指针运算符*或&后没有空格。

当声明一个指针变量或参数,你可以将星号放到相邻的任一类型或变量名:

1
2
3
4
5
6
7
8
9
10
// 这些都很好,空间之前。
char *c;
const string &str;
 
// 这些都很好,后面跟空格。
char* c;    // 记得做"char* c, *d, *e, ...;"!
const string& str;
 
char * c;  // 不好 - *两边空格
const string & str;  // 不好 - &两边空格

你应该这样做,始终在一个单一的文件,因此,修改现有文件时,使用的样式在该文件中。

布尔表达式

当你有一个布尔表达式,它是比标准线的长度长,将线一致的分开。

在这个例子中,逻辑AND运算符总是在行末端:

1
2
3
4
5
if (thisOneThing > thisOtherThing &&
    aThirdThing == aFourthThing &&
    yetAnother && lastOne) {
    ...
}

请注意,当代码在这个例子包,&&和逻辑AND运算符是在该行的末尾。这是谷歌的代码更常见,虽然在该行的开头包装的所有操作符也是允许的。随意明智地插入额外的括号,因为如果使用得当,对于增加可读性他们是非常有用的。另外请注意,你应该总是使用标点符号操作符,如&&和〜,而不是词操作符,如and和compl。

返回值

不要无谓地围绕的返回有括号的表达式。

使用括号中return expr;你只会在那里使用他们在x = expr;

1
2
3
4
5
6
return result;                  // 没有括号中的简单情况。
return (someLongCondition &&    // 括号很好使一个复杂的表达式更可读
        anotherCondition);     
 
return (value);                // 你不会写 var = (value);
return(result);                // return 不是一个函数

变量和数组初始化

你的选择是 =(), or {}

你可能在=(), 或 {}之间选择; 一下都是正确的:

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"};

当一个类型使用{},它接受一个initializer_list在其构造函数之一时要小心。该{}语法有时更喜欢initializer_list构造。要获得非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 (lopsidedScore) {
#if DISASTER_PENDING      // 正确 -- 在行首开始
    dropEverything();
# if NOTIFY               // 正确但不要求 -- #后面接空格
    notifyClient();
# endif
#endif
    backToNormal();
  }
1
2
3
4
5
6
7
// 不好 - 指令缩进
  if (lopsidedScore) {
    #if DISASTER_PENDING  // 错误!  "#if"应该在行首
    dropEverything();
    #endif                // 错误!  "#endif"不要缩进
    backToNormal();
  }

类格式

在类中,protected和private的顺序,每个缩进一个空格。

一个类声明的基本格式(缺少注释,请参阅类注释需要哪些意见的讨论)是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass : public OtherClass
{
public:      // 注意没有空格缩进!
    MyClass();  // 通常4个空格缩进
    explicit MyClass(int var);
    ~MyClass() {}
 
    void someFunction();
    void someFunctionThatDoesNothing() {
    }
 
    void setSomeVar(int var) { _someVar = var; }
    int getSomeVar() const return _someVar; }
 
private:
    bool someInternalFunction();
 
    int _someVar;
    int _someOtherVar;
    DISALLOW_COPY_AND_ASSIGN(MyClass);
};

注意事项:

  • 所有基类的名称应该和子类名在同一行,受80列的限制。
  • public:protected:, 和private: 关键字不应该缩进。
  • 除第一个实例,这些关键字应该在前面加一个空行。这条规则是可选的小类。
  • 这些关键词后不留下一个空行。
  • public:应该在最前面, 紧接着是protected:最后是private:
  • 看到声明顺序排序在每个部分规则声明。

构造函数初始化列表

构造函数初始化列表可以全写在一行中,或者写在多行,不带缩进。

两种好的初始化列表格式:

1
2
// When it all fits on one line:
MyClass::MyClass(int var) : _someVar(var), _someOtherVar(var + 1) {}

or

1
2
3
4
5
6
7
8
9
10
// When it requires multiple lines, indent zero spaces, putting the colon on
// the first initializer line, and commas in new lines if needed:
MyClass::MyClass(int var)
: _someVar(var)             // 0 space indent
, _someOtherVar(var + 1)    // lined up
{
    ...
    doSomething();
    ...
}

命名空间格式化

命名空间的内容不要缩进。

命名空间不增加新的缩进层级。例如:

1
2
3
4
5
6
7
namespace {
 
void foo() {  // Correct.  No extra indentation within namespace.
  ...
}
 
}  // namespace

命名空间内无需缩进。

1
2
3
4
5
6
7
8
namespace {
 
  // Wrong.  Indented when it should not be.
  void foo() {
    ...
  }
 
}  // namespace

当声明嵌套的命名空间时,每个命名空间占一行。

1
2
namespace foo {
namespace bar {

水平空白

因地制宜地使用行水平空白。行尾禁止有空格。

一般

1
2
3
4
5
6
7
8
9
10
11
12
13
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
15
16
17
18
if (b) {          // 条件和循环关键字后带空格
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) {  // 循环总是在分号后有空格
    ...                 // 在分号前也可以添加空格。
for (auto x : counts) {  // 基于范围的循环总是在冒号前后都有空格
    ...
}
switch (i) {
    case 1:         // `switch`的`case`中,冒号后面不带空格
    ...
    case 2: break;  // 如果冒号后有代码,那么冒号后面要有空格。

操作符

1
2
3
4
5
6
7
8
x = 0;              // 赋值操作符前后都有空格
x = -5;             // 一元操作符和其参数中间没有空格
++x;               
if (x && !y)
    ...
v = w * x + y / z;  // 二元操作符通常前后都有空格
v = w*x + y/z;      // 但是因子周围可以没有空格
v = w * (x + z);    // 括号里面不需要空格

模版和类型转换

1
2
3
4
5
6
7
vector<string> x;           // 尖括号(`<>`)里不需要空格;
y = static_cast<char*>(x);  // 类型转换的`<`前面或者`>(`中间没有空格。
vector<char *> x;           // 类型和指针符号之间可以有空格,
                            // 但是代码风格要保持一致。
set<list<string>> x;        // C++11代码中允许这样用。
set<list<string> > x;       // C++03需要在两个`>`之间有空格。
set< list<string> > x;      // 你也可以在两个`<`之间对称地添加空格

垂直空白

垂直空白越少越好。

这不仅仅是规则而是原则问题了:不必要的话就不要使用空行。尤其是:不要在两个函数定义之间空超过2行,函数体头、尾不要有空行,函数体中也不要随意添加空行。

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

使用空行时的经验法则:

  • 函数开始和结束的空行对提高代码可读性没啥作用
  • if-else的一个链内添加空行可以提高代码可读性。

例外的规则

上面描述的编码惯例是强制性的。然而,就像所有好的规则,这些有时会有例外,我们在这里讨论。

现存的不符合标准的代码

在处理代码时你可能会偏离规则,不符合这个风格指南。

如果你发现自己修改代码在编写本指南提供的规范以外,在那些代码你中可能不得不偏离这些规则为了与原有惯例保持一致。如果你在怀疑关于如何做到这一点,请原作者或人目前负责的代码。记住,一致性与原有的的一致性。

Windows代码

Windows程序员开发了他们自己的编码惯例,主要源自于约定在Windows头文件和其他微软的代码。我们想让人很容易理解你的代码,所以我们为家写了一套适合任何平台的c++在指南。

值得重申的一些指导,你可能会忘记如果你习惯于普通的Windows风格:

  • 不要使用匈牙利命名法(例如,命名一个整数为iNum)。使用谷歌的命名约定,包括.cpp扩展源文件。
  • Windows定义了许多原始类型的同义词,如双字、句柄,等等。这是完全可以接受的,并鼓励,当调用Windows API函数时你使用这些类型。即便如此,保持尽可能接近底层c++类型。例如,使用常量TCHAR *代替LPCTSTR。
  • 当微软Visual C++编译时,编译器警告3级或更高,并将所有警告作为错误。
  • 不要使用#ragma;而使用标准的谷歌include警卫。include警卫的路径应该是相对于你的项目树的顶端。
  • 实际上,不使用任何非标准扩展,如#pragma和使用declspec,除非一定要这么做。使用`declspec(dllimport)__declspec(dllexport)是允许的;然而,你必须通过宏如DLLIMPORTDLLEXPORTCC_DLL`来使用它,这样的话别人可以很容易地禁用扩展,如果他们分享代码。

然而,有几个规则,我们有时需要在Windows上打破:

    • 通常我们禁止使用多继承实现;然而,它需要在使用COM和一些ATL/WTL类是必要的。你可以使用多继承实现来实现COM或ATL/WTL类和接口。
    • 虽然你不应该在自己的代码中使用异常,它们广泛用于ATL和一些STL中,包括一个带有Visual C++。当使用ATL,你应该定义_ATL_NO_EXCEPTIONS来禁用例外。你应该弄清是否还可以禁用STL例外,但如果不是这样,在编译器打开异常也行。(注意,这只是让STL编译。你应该还没有编写异常处理代码)。
    • 通常使用预编译头文件的方法是包含一个头文件的每个源文件时,通常用一个名字像StdAfx.h或precompile.h。为了你的代码更容易与其他项目,避免包括这个文件明确(除了precompile.cpp),并使用自动/ FI编译器选项包括文件。
    • 资源标题,通常命名资源.h并只包含宏,不需要符合这些样式指南。
posted @ 2015-07-09 18:52  wiessharling  阅读(1680)  评论(0编辑  收藏  举报