Goggle C++ Style

谷歌C++风格指南

此份说明文档是基于Google C++ Style 2019/09月份的,部分参考中文版指南。

背景

C++是许多谷歌开源项目使用的主要开发语言之一。每个C++程序员都知道,这种语言有许多强大的功能,但是这种功能带来了复杂性,从而使代码更容易出现bug,更难以阅读和维护。

本指南的目标是通过详细描述编写C++代码时应该做和不应该做的事情来管理这种复杂性。这些规则的存在是为了保持代码库的可管理性,同时仍然允许程序员有效地使用C++语言特性。风格,也被称为可读性,是我们所称的惯例。“风格”这个术语有点用词不当,因为这些约定涉及的远不止源文件格式。

谷歌开发的大多数开源项目都符合本指南中的要求。

注意,本指南不是C++教程:我们假设读者熟悉该语言。

原则

  1. 这些代码风格规则必须是具有作用的。
  2. 优化读者的阅读体验。
  3. 与现有代码保持一致。
  4. 适当采用其他C++社区规则
  5. 避免不必要或者存在危险的构造
  6. 使代码容易维护
  7. 注意代码规模
  8. 注意优化代码

本文件旨在提供最大限度的指导和合理的限制。一如既往,常识和良好的品味应该占上风。通过这一点,我们特别提到了整个谷歌c++社区的既定约定,而不仅仅是您个人或团队的偏好。怀疑并不愿意使用聪明的或不寻常的构造:没有禁令并不等同于许可。运用你的判断,如果你不确定,请毫不犹豫地要求你的项目负责人提供额外的信息。

C++ 版本

目前,代码应该以c++17为目标,即,不应使用c++2x功能。本指南所针对的c++版本将随着时间的推移而(积极地)向前发展。

不要使用非标准扩展。

在您的项目中使用c++ 14c++ 17的特性之前,请考虑移植到其他环境。

头文件

通常,每个.cc文件都应该有一个关联的.h文件。有一些常见的例外,比如单元测试和只包含main()函数的小型.cc文件。

正确使用头文件可以极大地提高代码的可读性、大小和性能。

下面的规则将指导您了解使用头文件的各种陷阱。

自包含头文件

头文件应该是自包含的(独立编译),并以.h结尾。

用于包含的非头文件应该以.inc结尾,并且要谨慎使用。

所有头文件都应该是自包含的。用户和重构工具不应该遵守包含头的特殊条件。特别地,头文件应该有头文件保护,并包含它需要的所有其他头文件。

宁可将模板和内联函数的定义与其声明放在同一个文件中。这些构造的定义必须包含在使用它们的每个.cc文件中,否则程序可能无法在某些构建配置中链接。如果声明和定义在不同的文件中,包括前者,则应传递地包括后者。不要将这些定义移动到单独包含的头文件(-inl.h);这种做法在过去很常见,但现在已经不允许了。

作为一个例外,允许在实例化模板的惟一.cc文件中定义一个模板,该模板为所有相关的模板参数集显式实例化,或者是类的私有实现细节。

在极少数情况下,设计用来包含的文件不是自包含的。这些文件通常包含在不寻常的位置,比如另一个文件的中间。它们可能不使用头保护,也可能不包含先决条件。将这些文件命名为.inc扩展名。尽量少使用,尽可能使用自包含的头文件。

头文件保护符

所有头文件都应该有#define保护,以防止多个包含。符号名的格式应该是
<PROJECT>_<PATH>_<FILE>_H_

为了保证唯一性,它们应该基于项目源树中的完整路径。例如,foo项目中的foo/src/bar/bar.h文件应该具有以下保护:

1
2
3
4
5
6
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

...

#endif // FOO_BAR_BAZ_H_

前向声明

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

定义

“前向声明”是类、函数或模板的声明没有相关的定义。

优点

  • 前向声明可以节省编译时间,因为#include迫使编译器打开更多的文件并处理更多的输入。
  • 前向声明可以节省不必要的重新编译。由于头文件中发生了不相关的更改,#include迫使代码更频繁地重新编译。

缺点

  • 前向声明会隐藏依赖项,使用户代码在头文件更改时跳过必要的重新编译。
  • 对库的后续更改可能会破坏前向声明。函数和模板的前向声明会防止头文件引用者对其api进行其他方面兼容的更改,比如扩展参数类型、添加具有默认值的模板参数或迁移到新的名称空间。
  • 名称空间std::的向前声明会产生未定义的行为。
  • 很难确定是需要一个前向声明还是一个完整的#include。用前向声明替换#include会隐式地改变代码的含义:
1
2
3
4
5
6
7
8
9
// b.h:
struct B {};
struct D : B {};

// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); } // calls f(B*)

如果将#include替换为B和D的前向声明, test()将调用f(void*)。

  • 从标头中前向声明多个符号可能比简单地包含标头更冗长。
  • 构造代码以支持前向声明(例如,使用指针成员而不是对象成员)可能会使代码更慢、更复杂。

结论

  • 尽量避免在另一个项目中定义实体的前向声明。
  • 当使用头文件中声明的函数时,始终使用#include来包含该头文件。
  • 当使用类模板时,最好使用#include包含它的头文件。

内联函数

只在函数很小的时候定义内联函数,比如10行或更少

定义

您可以声明函数的方式允许编译器在代码中该函数使用位置以内联方式展开它们,而不是通过通常的函数调用机制调用它们。

优点

内联一个函数可以生成更有效的目标代码,只要内联函数很小。可以随意使用内联访问器和修改器,以及其他简短的、性能关键的函数。

缺点

过度使用内联实际上会使程序变慢。根据函数的大小,内联它会导致代码大小增加或减少。内联一个非常小的访问函数通常会减少代码大小,而内联一个非常大的函数可以显著增加代码大小。在现代处理器上,由于更好地使用指令缓存,较小的代码通常运行得更快。

结论

一个不错的经验法则是,如果一个函数超过10行,就不要内联它。注意析构函数,由于隐式成员和基析构函数调用,析构函数通常比它们的显示时间长!

另一个有用的经验法则:使用循环或switch语句内联函数通常没有成本效益(除非,循环或switch语句永远不会执行)。

重要的是要知道,函数并不总是内联的,即使它们是这样声明的;例如,虚函数和递归函数通常不内联。通常递归函数不应该内联。将虚拟函数内联的主要原因是将它的定义放在类中,或者是为了方便,或者是为了记录它的行为,例如访问器和变量。

Include的名称和顺序

按以下顺序包含头:相关头文件、C系统头文件、c++标准库头文件、其他依赖库头文件、项目头文件。

项目的所有头文件都应该作为项目源目录的后代列出,而不使用UNIX目录别名。(当前目录)或..(父目录)。例如,google-awesome-project/src/base/logging.h应该包含如下内容:

1
#include "base/logging.h"

dir/foo.cc 或者 dir/foo_test.cc的主要目的是实现或测试dir2/foo2中的内容。include顺序如下:

1
2
3
4
5
6
7
8
9
#include "dir2/foo2.h"

#include <> // C system headers

#include <> // C++ standard library headers

#include <> // Other libraries' .h files

#include "" // Your project's .h files.

用一行空白分隔每个非空组。

这样的顺序在程序编译出错的时候,如果dir2/foo2.h出错,那么编译报错的结果会首先显示这些文件的错误。不至于牵扯到其他人。

注意,C头文件(如stdf.h)本质上可以与c++头文件(cstddef)互换。这两种风格都可以接受,但最好与现有代码保持一致。

在每个部分中,include应该按字母顺序排列。注意,旧的代码可能不符合这个规则,应该在方便的时候进行修复。

您应该包含所依赖的所有头文件,除非在不寻常的前向声明情况下。如果你依赖于bar.h中的符号,不要指望您所包含的foo.h可以包含bar.h中的符号,即便它(当前)包含bar.h:要自己包含bar.h,除非foo.h明确地表明它打算为您提供bar.h中的符号。

例如,google-awesome-project/src/foo/internal/fooserver.cc可能是这样的:

1
2
3
4
5
6
7
8
9
10
11
#include "foo/server/fooserver.h"

#include <sys/types.h>
#include <unistd.h>

#include <string>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/server/bar.h"

此外

有时候,系统特定代码需要条件包含。这样的代码可以将条件包含放在其他包含之后。当然,要保持特定于系统的代码小而本地化。例子:

1
2
3
4
5
6
7
#include "foo/public/fooserver.h"

#include "base/port.h" // For LANG_CXX11.

#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11

作用域

命名空间

定义

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

优点

  1. 虽然类已经提供了(可嵌套的)命名轴线 (YuleFox 注: 将命名分割在不同类的作用域内), 命名空间在这基础上又封装了一层.

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

  3. 内联命名空间会自动把内部的标识符放到外层作用域,比如:

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

X::Y::foo()X::foo()彼此可代替。内联命名空间主要用来保持跨版本的 ABI 兼容性。

缺点

  1. 命名空间具有迷惑性, 因为它们使得区分两个相同命名所指代的定义更加困难。

  2. 内联命名空间很容易令人迷惑,毕竟其内部的成员不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用。

  3. 有时候不得不多次引用某个定义在许多嵌套命名空间里的实体,使用完整的命名空间会导致代码的冗长。

  4. 在头文件中使用匿名空间导致违背 C++ 的唯一定义原则 (One Definition Rule (ODR)).

结论

根据下文将要提到的策略合理使用命名空间.

  1. 遵守 命名空间命名 中的规则。

  2. 像之前的几个例子中一样,在命名空间的最后注释出命名空间的名字。

  3. 用命名空间把文件包含, gflags 的声明/定义,以及类的前置声明以外的整个源文件封装起来, 以区别于其它命名空间:

1
2
3
4
5
6
7
8
9
10
11
12
// .h 文件
namespace mynamespace {

// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
public:
...
void Foo();
};

} // namespace mynamespace

更复杂的 .cc 文件包含更多, 更复杂的细节, 比如 gflags 或 using 声明。

1
2
3
4
5
6
7
8
9
#include "a.h"

DEFINE_FLAG(bool, someflag, false, "dummy flag");

namespace a {

...code for a... // 左对齐

} // namespace a
  1. 不要在命名空间 std 内声明任何东西, 包括标准库的类前置声明. 在 std 命名空间声明实体是未定义的行为, 会导致如不可移植. 声明标准库下的实体, 需要包含对应的头文件.
  2. 不应该使用 using 指示 引入整个命名空间的标识符号。
1
2
// 禁止 —— 污染命名空间
using namespace foo;
  1. 不要在头文件中使用 命名空间别名 除非显式标记内部命名空间使用。因为任何在头文件中引入的命名空间都会成为公开API的一部分。
1
2
// 在 .cc 中使用别名缩短常用的命名空间
namespace baz = ::foo::bar::baz;
1
2
3
4
5
6
7
8
9
10
11
12
// 在 .h 中使用别名缩短常用的命名空间
namespace librarian {
namespace impl { // 仅限内部使用
namespace sidetable = ::pipeline_diagnostics::sidetable;
} // namespace impl

inline void my_inline_function() {
// 限制在一个函数中的命名空间别名
namespace baz = ::foo::bar::baz;
...
}
} // namespace librarian

未命名的命名空间和静态变量

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

定义

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

结论

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

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

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

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

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

优点

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

缺点

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

结论

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

1
2
3
4
5
6
namespace myproject {
namespace foo_bar {
void Function1();
void Function2();
} // namespace foo_bar
} // namespace myproject

而非

1
2
3
4
5
6
7
namespace myproject {
class FooBar {
public:
static void Function1();
static void Function2();
};
} // namespace myproject

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

如果你必须定义非成员函数, 又只是在 .cc 文件中使用它, 可使用匿名 2.1. 命名空间 或 static 链接关键字 (如 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 一开始就初始化

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

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

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

1
2
3
4
5
// 低效的实现
for (int i = 0; i < 1000000; ++i) {
Foo f; // 构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}

在循环作用域外面声明这类变量要高效的多:

1
2
3
4
Foo f;                      // 构造函数和析构函数只调用 1 次
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}

静态变量和全局变量

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

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

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

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

同一个编译单元内是明确的,静态初始化优先于动态初始化,初始化顺序按照声明顺序进行,销毁则逆序。不同的编译单元之间初始化和销毁顺序属于未明确行为 (unspecified behaviour)。

同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从 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 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。

上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量, 以及函数静态变量.

译者 (YuleFox) 笔记

  1. .cc 中的匿名命名空间可避免命名冲突, 限定作用域, 避免直接使用 using 关键字污染命名空间;
  2. 嵌套类符合局部使用原则, 只是不能在其他头文件中前置声明, 尽量不要 public;
  3. 尽量不用全局函数和全局变量, 考虑作用域和命名空间限制, 尽量单独形成编译单元;
  4. 多线程中的全局变量 (含静态成员变量) 不要使用 class 类型 (含 STL 容器), 避免不明确行为导致的 bug.
  5. 作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率.

译者(acgtyrant)笔记

  1. 注意「using 指示(using-directive)」和「using 声明(using-declaration)」的区别。
  2. 匿名命名空间说白了就是文件作用域,就像 C static 声明的作用域一样,后者已经被 C++ 标准提倡弃用。
  3. 局部变量在声明的同时进行显式值初始化,比起隐式初始化再赋值的两步过程要高效,同时也贯彻了计算机体系结构重要的概念「局部性(locality)」。
  4. 注意别在循环犯大量构造和析构的低级错误。

thread_local变量

未在函数中声明的thread_local变量必须使用真正的编译时常量初始化,并且必须使用ABSL_CONST_INIT属性强制执行。与定义线程本地数据的其他方法相比,应更倾向使用thread_local

定义

从c++ 11开始,变量可以用thread_local说明符声明:

1
thread_local Foo foo = ...;

这样的变量实际上是对象的集合,因此当不同的线程访问它时,它们实际上是在访问不同的对象。thread_local变量在很多方面很像静态存储持续时间变量。例如,它们可以在名称空间范围内、函数内部或作为静态类成员声明,但不能作为普通类成员。

thread_local变量实例的初始化非常类似于静态变量,只是它们必须为每个线程单独初始化,而不是在程序启动时初始化一次。这意味着函数中声明的thread_local变量是安全的,但是其他thread_local变量与静态变量(以及更多其他变量)面临相同的初始化顺序问题。

thread_local变量实例在线程终止时被销毁,因此它们不存在静态变量的销毁顺序问题。

优点

  1. 线程本地数据本质上是安全的(因为通常只有一个线程可以访问它),这使得thread_local对于并发编程非常有用。
  2. thread_local是创建线程本地数据的唯一受标准支持的方法。

缺点

  1. 访问thread_local变量可能会触发执行不可预测和不可控的其他代码量。
  2. thread_local变量是有效的全局变量,除了缺乏线程安全性之外,它还具有全局变量的所有缺点。
  3. thread_local变量所消耗的内存随着运行线程的数量而变化(在最坏的情况下),这在程序中可能非常大。
  4. 普通类成员不能是thread_local
  5. thread_local可能没有某些编译器内部函数那么高效。

类是 C++ 中代码的基本单元. 显然, 它们被广泛使用. 本节列举了在写一个类时的主要注意事项.

构造函数的职责

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

定义

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

优点

  1. 无需考虑类是否被初始化.
  2. 经过构造函数完全初始化后的对象可以为 const 类型, 也能更方便地被标准容器或算法使用.

缺点

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

结论

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

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

隐式转换

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

定义

隐式类型转换允许一个某种类型 (称作 源类型) 的对象被用于需要另一种类型 (称作 目的类型) 的位置, 例如, 将一个 int 类型的参数传递给需要 double 类型的函数.

除了语言所定义的隐式类型转换, 用户还可以通过在类定义中添加合适的成员定义自己需要的转换. 在源类型中定义隐式类型转换, 可以通过目的类型名的类型转换运算符实现 (例如 operator bool()). 在目的类型中定义隐式类型转换, 则通过以源类型作为其唯一参数 (或唯一无默认值的参数) 的构造函数实现.

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

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

void Func(Foo f);

此时,下面的代码是不允许的

1
Func({42, 3.14});  // Error

这一代码从技术上说并非隐式类型转换, 但是语言标准认为这是 explicit 应当限制的行为.

优点

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

缺点

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

结论

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

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

可复制和可移动的类型

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

定义

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

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

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

优点

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

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

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

缺点

  1. 许多类型都不需要拷贝, 为它们提供拷贝操作会让人迷惑, 也显得荒谬而不合理. 单件类型 (Registerer), 与特定的作用域相关的类型 (Cleanup), 与其他对象实体紧耦合的类型 (Mutex) 从逻辑上来说都不应该提供拷贝操作. 为基类提供拷贝 / 赋值操作是有害的, 因为在使用它们时会造成 对象切割 . 默认的或者随意的拷贝操作实现可能是不正确的, 这往往导致令人困惑并且难以诊断出的错误.
  2. 拷贝构造函数是隐式调用的, 也就是说, 这些调用很容易被忽略. 这会让人迷惑, 尤其是对那些所用的语言约定或强制要求传引用的程序员来说更是如此. 同时, 这从一定程度上说会鼓励过度拷贝, 从而导致性能上的问题.

结论

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

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

1
2
3
4
5
6
7
8
class Foo {
public:
Foo(Foo&& other) : field_(other.field) {}
// 差, 只定义了移动构造函数, 而没有定义对应的赋值运算符.

private:
Field field_;
};

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

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

1
2
3
// MyClass is neither copyable nor movable.
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;

结构体与类

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

说明

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

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

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

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

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

结构体,对,元组

当元素可以有有意义的名称时,最好使用结构而不是对或元组。

虽然使用对和元组可以避免定义自定义类型的需要,从而节省编写代码时的工作,但是在读取代码时,有意义的字段名几乎总是比.first.secondstd::get<X>清楚得多。虽然c++ 14引入std::get<Type>来按类型而不是索引访问元组元素(当类型是惟一的时)有时可以部分缓解这种情况,但是字段名通常比类型更清晰,信息更丰富。

在对或元组的元素没有特定含义的泛型代码中,对和元组可能是合适的。为了与现有代码或api互操作,可能还需要使用它们。

继承

使用组合 (YuleFox 注: 这一点也是 GoF 在 <> 里反复强调的) 常常比使用继承更合理. 如果使用继承的话, 定义为 public 继承.

定义

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

优点

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

缺点

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

结论

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

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

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

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

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

运算符重载

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

定义

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

优点

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

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

缺点

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

结论

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

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

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

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

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

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

存取控制

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

声明顺序

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

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

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

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

译者 (YuleFox) 笔记

  1. 不在构造函数中做太多逻辑相关的初始化;
  2. 编译器提供的默认构造函数不会对变量进行初始化, 如果定义了其他构造函数, 编译器不再提供, 需要编码者自行提供默认构造函数;
  3. 为避免隐式转换, 需将单参数构造函数声明为 explicit;
  4. 为避免拷贝构造函数, 赋值操作的滥用和编译器自动生成, 可将其声明为 private 且无需实现;
    仅在作为数据集合时使用 struct;
  5. 组合 > 实现继承 > 接口继承 > 私有继承, 子类重载的虚函数也要声明 virtual 关键字, 虽然编译器允许不这样做;
  6. 避免使用多重继承, 使用时, 除一个基类含有实现外, 其他基类均为纯接口;
  7. 接口类类名以 Interface 为后缀, 除提供带实现的虚析构函数, 静态成员函数外, 其他均为纯虚函数, 不定义非静态数据成员, 不提供构造函数, 提供的话, 声明为 protected;
  8. 为降低复杂性, 尽量不重载操作符, 模板, 标准类中使用时提供文档说明;
  9. 存取函数一般内联在头文件中;
  10. 声明次序: public -> protected -> private;
    函数体尽量短小, 紧凑, 功能单一;

函数

返回参数

c++函数的输出自然通过返回值提供,有时通过输出参数提供。

使用返回值而不是输出参数:它们提高了可读性,并且通常提供相同或更好的性能。如果只使用输出参数,则它们应该出现在输入参数之后。

参数要么是函数的输入,要么是函数的输出,或者两者都是。输入参数通常是值或const引用,而输出和输入/输出参数将是指向非const的指针。

当对函数参数进行排序时,将所有只输入的参数放在前面

编写短小函数

我们倾向于编写简短, 凝练的函数.

说明

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

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

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

引用参数

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

定义

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

优点

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

缺点

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

结论

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

void Foo(const string &in, string *out);
事实上这在 Google Code 是一个硬性约定: 输入参数是值参或 const 引用, 输出参数为指针. 输入参数可以是 const 指针, 但决不能是非 const 的引用参数, 除非特殊要求, 比如 swap().

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

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

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

函数重载

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

定义

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

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

优点

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

缺点

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

结论

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

缺省参数

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

优点

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

缺点

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

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

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

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

结论

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

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

函数返回类型后置语法

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

定义

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

int foo(int x);

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

auto foo(int x) -> int;

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

优点

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

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

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

对比下面的例子:

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

缺点

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

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

结论

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

谷歌编码经验

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

所有权和智能指针

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

定义

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

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

优点

  1. 如果没有清晰、逻辑条理的所有权安排, 不可能管理好动态分配的内存.
  2. 传递对象的所有权, 开销比复制来得小, 如果可以复制的话.
  3. 传递所有权也比”借用”指针或引用来得简单, 毕竟它大大省去了两个用户一起协调对象生命周期的工作.
  4. 如果所有权逻辑条理, 有文档且不紊乱的话, 可读性会有很大提升.
  5. 可以不用手动完成所有权的登记工作, 大大简化了代码, 也免去了一大波错误之恼.
  6. 对于 const 对象来说, 智能指针简单易用, 也比深度复制高效.

缺点

  1. 不得不用指针(不管是智能的还是原生的)来表示和传递所有权. 指针语义可要比值语义复杂得许多了, 特别是在 API 里:这时不光要操心所有权, 还要顾及别名, 生命周期, 可变性以及其它大大小小的问题.
  2. 其实值语义的开销经常被高估, 所以所有权传递带来的性能提升不一定能弥补可读性和复杂度的损失.
  3. 如果 API 依赖所有权的传递, 就会害得客户端不得不用单一的内存管理模型.
  4. 如果使用智能指针, 那么资源释放发生的位置就会变得不那么明显.
  5. std::unique_ptr 的所有权传递原理是 C++11 的 move 语法, 后者毕竟是刚刚推出的, 容易迷惑程序员.
  6. 如果原本的所有权设计已经够完善了, 那么若要引入所有权共享机制, 可能不得不重构整个系统.
  7. 所有权共享机制的登记工作在运行时进行, 开销可能相当大.
  8. 某些极端情况下 (例如循环引用), 所有权被共享的对象永远不会被销毁.
  9. 智能指针并不能够完全代替原生指针.

结论

如果必须使用动态分配, 那么更倾向于将所有权保持在分配者手中. 如果其他地方要使用这个对象, 最好传递它的拷贝, 或者传递一个不用改变所有权的指针或引用. 倾向于使用 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.

译者(acgtyrant)笔记

  1. 把智能指针当成对象来看待的话, 就很好领会它与所指对象之间的关系了.
  2. 原来 RustOwnership 思想是受到了 C++ 智能指针的很大启发啊.
  3. scoped_ptrauto_ptr 已过时. 现在是 shared_ptruniqued_ptr 的天下了.
  4. 按本文来说, 似乎除了智能指针, 还有其它所有权机制, 值得留意.
  5. Arch Linux 用户注意了, AUR 有对 cpplint 打包.

其他c++的特性

右值引用

只在定义移动构造函数与移动赋值操作时使用右值引用. 不要使用 std::forward.

定义

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

优点

用于定义移动构造函数 (使用类的右值引用进行构造的函数) 使得移动一个值而非拷贝之成为可能. 例如, 如果 v1 是一个 vector<string>, 则 auto v2(std::move(v1)) 将很可能不再进行大量的数据复制而只是简单地进行指针操作, 在某些情况下这将带来大幅度的性能提升.

右值引用使得编写通用的函数封装来转发其参数到另外一个函数成为可能, 无论其参数是否是临时对象都能正常工作.

右值引用能实现可移动但不可拷贝的类型, 这一特性对那些在拷贝方面没有实际需求, 但有时又需要将它们作为函数参数传递或塞入容器的类型很有用.

要高效率地使用某些标准库类型, 例如 std::unique_ptr, std::move 是必需的.

缺点

右值引用是一个相对比较新的特性 (由 C++11 引入), 它尚未被广泛理解. 类似引用崩溃, 移动构造函数的自动推导这样的规则都是很复杂的.

结论

只在定义移动构造函数与移动赋值操作时使用右值引用, 不要使用 std::forward 功能函数. 你可能会使用 std::move 来表示将值从一个对象移动而不是复制到另一个对象.

友元

我们允许合理的使用友元类及友元函数.

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

友元扩大了 (但没有打破) 类的封装边界. 某些情况下, 相对于将类成员声明为 public, 使用友元是更好的选择, 尤其是如果你只允许另一个类访问该类的私有成员时. 当然, 大多数类都只应该通过其提供的公有成员进行互操作.

异常处理

我们不使用 C++ 异常.

优点

  1. 异常允许应用高层决定如何处理在底层嵌套函数中「不可能发生」的失败(failures),不用管那些含糊且容易出错的错误代码(acgtyrant 注:error code, 我猜是C语言函数返回的非零 int 值)。
  2. 很多现代语言都用异常。引入异常使得 C++ 与 Python, Java 以及其它类 C++ 的语言更一脉相承。
  3. 有些第三方 C++ 库依赖异常,禁用异常就不好用了。
  4. 异常是处理构造函数失败的唯一途径。虽然可以用工厂函数(acgtyrant 注:factory function, 出自 C++ 的一种设计模式,即「简单工厂模式」)或 Init() 方法代替异常, 但是前者要求在堆栈分配内存,后者会导致刚创建的实例处于 ”无效“ 状态。
  5. 在测试框架里很好用。

缺点

  1. 在现有函数中添加 throw 语句时,您必须检查所有调用点。要么让所有调用点统统具备最低限度的异常安全保证,要么眼睁睁地看异常一路欢快地往上跑,最终中断掉整个程序。举例,f() 调用 g(), g() 又调用 h(), 且 h 抛出的异常被 f 捕获。当心 g, 否则会没妥善清理好。

2.. 还有更常见的,异常会彻底扰乱程序的执行流程并难以判断,函数也许会在您意料不到的地方返回。您或许会加一大堆何时何处处理异常的规定来降低风险,然而开发者的记忆负担更重了。

  1. 异常安全需要RAII和不同的编码实践. 要轻松编写出正确的异常安全代码需要大量的支持机制. 更进一步地说, 为了避免读者理解整个调用表, 异常安全必须隔绝从持续状态写到 “提交” 状态的逻辑. 这一点有利有弊 (因为你也许不得不为了隔离提交而混淆代码). 如果允许使用异常, 我们就不得不时刻关注这样的弊端, 即使有时它们并不值得.

  2. 启用异常会增加二进制文件数据,延长编译时间(或许影响小),还可能加大地址空间的压力。

  3. 滥用异常会变相鼓励开发者去捕捉不合时宜,或本来就已经没法恢复的「伪异常」。比如,用户的输入不符合格式要求时,也用不着抛异常。如此之类的伪异常列都列不完。

结论

从表面上看来,使用异常利大于弊, 尤其是在新项目中. 但是对于现有代码, 引入异常会牵连到所有相关代码. 如果新项目允许异常向外扩散, 在跟以前未使用异常的代码整合时也将是个麻烦. 因为 Google 现有的大多数 C++ 代码都没有异常处理, 引入带有异常处理的新代码相当困难.

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

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

对于 Windows 代码来说, 有个 特例.

(YuleFox 注: 对于异常处理, 显然不是短短几句话能够说清楚的, 以构造函数为例, 很多 C++ 书籍上都提到当构造失败时只有异常可以处理, Google 禁止使用异常这一点, 仅仅是为了自身的方便, 说大了, 无非是基于软件管理成本上, 实际使用中还是自己决定)

noexcept

少用

定义

noexcept说明符用于指定函数是否抛出异常。如果异常从标记为noexcept的函数转义,程序将通过std::terminate崩溃。

noexcept操作符执行编译时检查,如果声明表达式不抛出任何异常,则返回true。

优点

在某些情况下,将move构造函数指定为noexcept可以提高性能,例如std::vector<T>::resize()在T的move构造函数为noexcept时移动对象,而不是复制对象。

在启用异常的环境中,在函数上指定noexcept可以触发编译器优化,例如,如果编译器知道noexcept说明符不会引发异常,那么它不必为堆栈展开生成额外的代码。

缺点

在禁用异常的遵循本指南的项目中,很难确保except说明符都是正确的,甚至很难定义正确意味着什么。

如果不是不可能,也很难撤消任何操作,因为它消除了调用者可能依赖的保证,而这种保证很难检测到。

总结

如果它准确地反映了函数的预期语义,也就是说,如果一个异常在函数体中以某种方式抛出,那么它就代表了一个致命的错误。
您可以假设,除了on move构造函数外,没有其他构造函数具有显著的性能优势。
如果您认为除了某些其他功能外,不指定其他功能对性能有显著的好处,请与项目负责人讨论。

除非异常被完全禁用(即大多数谷歌c++环境),否则选择无条件no_2。
否则,使用带有简单条件的条件noexcept说明符,仅在函数可能抛出的少数情况下计算false。
测试可能包括类型特征,检查所涉及的操作是否可能抛出(例如std::is_nothrow_move_constructible for move- construction objects),或者分配是否可以抛出(例如:absl::default_allocator_is_nothrow for standard default allocation)。
注意在许多情况下,只有一个异常的可能原因是分配失败(我们相信移动构造函数不应该扔由于分配失败除外),还有许多应用程序的适当治疗内存耗尽问题作为一个致命错误,而不是一个异常条件,你的程序应该试图恢复。
即使对其他潜在的失败应该优先考虑界面简单支持所有可能的异常抛出场景:而不是写一个复杂noexcept条款,取决于一个哈希函数可以扔,例如,简单的文档,您的组件不支持哈希函数,使其无条件noexcept。

运行时类型信息(RTTI)

我们禁止使用 RTTI.

定义

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

优点

RTTI 的标准替代 (下面将描述) 需要对有问题的类层级进行修改或重构. 有时这样的修改并不是我们所想要的, 甚至是不可取的, 尤其是在一个已经广泛使用的或者成熟的代码中.

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

在考虑多个抽象对象时 RTTI 也很好用. 例如:

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

缺点

在运行时判断类型通常意味着设计问题. 如果你需要在运行期间确定一个对象的类型, 这通常说明你需要考虑重新设计你的类.

随意地使用 RTTI 会使你的代码难以维护. 它使得基于类型的判断树或者 switch 语句散布在代码各处. 如果以后要进行修改, 你就必须检查它们.

结论

RTTI 有合理的用途但是容易被滥用, 因此在使用时请务必注意. 在单元测试中可以使用 RTTI, 但是在其他代码中请尽量避免. 尤其是在新代码中, 使用 RTTI 前务必三思. 如果你的代码需要根据不同的对象类型执行不同的行为的话, 请考虑用以下的两种替代方案之一查询类型:

虚函数可以根据子类类型的不同而执行不同代码. 这是把工作交给了对象本身去处理.

如果这一工作需要在对象之外完成, 可以考虑使用双重分发的方案, 例如使用访问者设计模式. 这就能够在对象之外进行类型判断.

如果程序能够保证给定的基类实例实际上都是某个派生类的实例, 那么就可以自由使用 dynamic_cast. 在这种情况下, 使用dynamic_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++ 风格.

static_cast 替代 C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时.
const_cast 去掉 const 限定符.
reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换. 仅在你对所做一切了然于心时使用.
至于 dynamic_cast 参见 6.8. 运行时类型识别.

只在记录日志时使用流.

定义

流用来替代 printf()scanf().

优点

有了流, 在打印时不需要关心对象的类型. 不用担心格式化字符串与参数列表不匹配 (虽然在 gcc 中使用 printf 也不存在这个问题). 流的构造和析构函数会自动打开和关闭对应的文件.

缺点

流使得 pread() 等功能函数很难执行. 如果不使用 printf 风格的格式化字符串, 某些格式化操作 (尤其是常用的格式字符串 %.*s) 用流处理性能是很低的. 流不支持字符串操作符重新排序 (%1s), 而这一点对于软件国际化很有用.

结论

不要使用流, 除非是日志接口需要. 使用 printf 之类的代替.

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

拓展讨论

对这一条规则存在一些争论, 这儿给出点深层次原因. 回想一下唯一性原则 (Only One Way): 我们希望在任何时候都只使用一种确定的 I/O 类型, 使代码在所有 I/O 处都保持一致. 因此, 我们不希望用户来决定是使用流还是 printf + read/write. 相反, 我们应该决定到底用哪一种方式. 把日志作为特例是因为日志是一个非常独特的应用, 还有一些是历史原因.

流的支持者们主张流是不二之选, 但观点并不是那么清晰有力. 他们指出的流的每个优势也都是其劣势. 流最大的优势是在输出时不需要关心打印对象的类型. 这是一个亮点. 同时, 也是一个不足: 你很容易用错类型, 而编译器不会报警. 使用流时容易造成的这类错误:

1
2
cout << this;   // 输出地址
cout << *this; // 输出值

由于 << 被重载, 编译器不会报错. 就因为这一点我们反对使用操作符重载.

有人说 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 参数 (否则变量需要 const_cast 类型转换), 在调用库函数时显得尤其麻烦.

结论

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

如果函数不会修改传你入的引用或指针类型参数, 该参数应声明为 const.
尽可能将函数声明为 const. 访问函数应该总是 const. 其他不会修改任何数据成员, 未调用非 const 函数, 不会返回数据成员非 const 指针或引用的函数也应该声明成 const.
如果数据成员在对象构造之后不再发生变化, 可将其定义为 const.
然而, 也不要发了疯似的使用 const. 像 const int * const * const x; 就有些过了, 虽然它非常精确的描述了常量 x. 关注真正有帮助意义的信息: 前面的例子写成 const int** x 就够了.

关键字 mutable 可以使用, 但是在多线程中是不安全的, 使用时首先要考虑线程安全.

const 的位置:

有人喜欢 int const *foo 形式, 不喜欢 const int* foo, 他们认为前者更一致因此可读性也更好: 遵循了 const 总位于其描述的对象之后的原则. 但是一致性原则不适用于此, “不要过度使用” 的声明可以取消大部分你原本想保持的一致性. 将 const 放在前面才更易读, 因为在自然语言中形容词 (const) 是在名词 (int) 之前.

这是说, 我们提倡但不强制 const 在前. 但要保持代码的一致性! (Yang.Y 注: 也就是不要在一些地方把 const 写在类型前面, 在其他地方又写在后面, 确定一种写法, 然后保持一致.)

constexpr使用

在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化。

定义

变量可以被声明成 constexpr 以表示它是真正意义上的常量,即在编译时和运行时都不变。函数或构造函数也可以被声明成 constexpr, 以用来定义 constexpr 变量。

优点

如今 constexpr 就可以定义浮点式的真・常量,不用再依赖字面值了;也可以定义用户自定义类型上的常量;甚至也可以定义函数调用所返回的常量。

缺点

若过早把变量优化成 constexpr 变量,将来又要把它改为常规变量时,挺麻烦的;当前对constexpr函数和构造函数中允许的限制可能会导致这些定义中解决的方法模糊。

结论

constexpr 特性,方才实现了 C++ 在接口上打造真正常量机制的可能。好好用 constexpr 来定义真・常量以及支持常量的函数。避免复杂的函数定义,以使其能够与constexpr一起使用。 千万别痴心妄想地想靠 constexpr 来强制代码「内联」。

整型

C++ 内建整型中, 仅使用 int. 如果程序中需要不同大小的变量, 可以使用 <stdint.h> 中长度精确的整型, 如 int16_t.如果您的变量可能不小于 2^31 (2GiB), 就用 64 位变量比如 int64_t. 此外要留意,哪怕您的值并不会超出 int 所能够表示的范围,在计算过程中也可能会溢出。所以拿不准时,干脆用更大的类型。

定义

C++ 没有指定整型的大小. 通常人们假定 short16 位, int32 位, long32 位, long long64 位.

优点

保持声明统一.

缺点

C++ 中整型大小因编译器和体系结构的不同而不同.

结论

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

如果已知整数不会太大, 我们常常会使用 int, 如循环计数. 在类似的情况下使用原生类型 int. 你可以认为 int 至少为 32 位, 但不要认为它会多于 32 位. 如果需要 64 位整型, 用 int64_tuint64_t.

对于大整数, 使用 int64_t.

不要使用 uint32_t 等无符号整型, 除非你是在表示一个位组而不是一个数值, 或是你需要定义二进制补码溢出. 尤其是不要为了指出数值永不会为负, 而使用无符号类型. 相反, 你应该使用断言来保护数据.

如果您的代码涉及容器返回的大小(size),确保其类型足以应付容器各种可能的用法。拿不准时,类型越大越好。

小心整型类型转换和整型提升(acgtyrant 注:integer promotions, 比如 intunsigned int 运算时,前者被提升为 unsigned int 而有可能溢出),总有意想不到的后果。

关于无符号整数:

有些人, 包括一些教科书作者, 推荐使用无符号类型表示非负数. 这种做法试图达到自我文档化. 但是, 在 C 语言中, 这一优点被由其导致的 bug 所淹没. 看看下面的例子:

for (unsigned int i = foo.Length()-1; i >= 0; --i) ...

上述循环永远不会退出! 有时 gcc 会发现该 bug 并报警, 但大部分情况下都不会. 类似的 bug 还会出现在比较有符合变量和无符号变量时. 主要是 C 的类型提升机制会致使无符号类型的行为出乎你的意料.

因此, 使用断言来指出变量为非负数, 而不是使用无符号型!

64位的可移植性

代码应该对 64 位和 32 位系统友好. 处理打印, 比较, 结构体对齐时应切记:

  • 对于某些类型, 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
// 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" C99 规定 %zu
ptrdiff_t %d %"PRIdS" C99 规定 %zd

注意 PRI* 宏会被编译器扩展为独立字符串. 因此如果使用非常量的格式化字符串, 需要将宏的值而不是宏名插入格式中. 使用 PRI* 宏同样可以在 % 后包含长度指示符. 例如, printf("x = %30"PRIuS"\n", x) 在 32 位 Linux 上将被展开为 printf("x = %30" "u" "\n", x), 编译器当成 printf("x = %30u\n", x) 处理 (Yang.Y 注: 这在 MSVC 6.0 上行不通, VC 6 编译器不会自动把引号间隔的多个字符串连接一个长字符串).

  • 记住 sizeof(void *) != sizeof(int). 如果需要一个指针大小的整数要用 intptr_t.
  • 你要非常小心的对待结构体对齐, 尤其是要持久化到磁盘上的结构体 (Yang.Y 注: 持久化 - 将数据按字节流顺序保存在磁盘文件或数据库中). 在 64 位系统中, 任何含有 int64_t/uint64_t 成员的类/结构体, 缺省都以 8 字节在结尾对齐. 如果 32 位和 64 位代码要共用持久化的结构体, 需要确保两种体系结构下的结构体对齐一致. 大多数编译器都允许调整结构体对齐. gcc 中可使用 __attribute__((packed)). MSVC 则提供了 #pragma pack()__declspec(align()) (YuleFox 注, 解决方案的项目属性里也可以直接设置).
  • 创建 64 位常量时使用 LL 或 ULL 作为后缀, 如:
1
2
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;
  • 如果你确实需要 32 位和 64 位系统具有不同代码, 可以使用 #ifdef _LP64 指令来切分 32/64 位代码. (尽量不要这么做, 如果非用不可, 尽量使修改局部化)

预处理宏

使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之.

宏意味着你和编译器看到的代码是不同的. 这可能会导致异常行为, 尤其因为宏具有全局作用域.

值得庆幸的是, C++ 中, 宏不像在 C 中那么必不可少. 以往用宏展开性能关键的代码, 现在可以用内联函数替代. 用宏表示常量可被 const 变量代替. 用宏 “缩写” 长变量名可被引用代替. 用宏进行条件编译… 这个, 千万别这么做, 会令测试更加痛苦 (#define 防止头文件重包含当然是个特例).

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

下面给出的用法模式可以避免使用宏带来的问题; 如果你要宏, 尽可能遵守:

  • 不要在 .h 文件中定义宏.
  • 在马上要使用时才进行 #define, 使用后要立即 #undef.
  • 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;
  • 不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为.
  • 不要用 ## 处理函数,类和变量的名字。

0/nullptr/NULL

整数用0,实数用0.0,指针用nullptr或者NULL,字符串用’\0’。

c++11用nullptr

sizeof

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

使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新. 您或许会用 sizeof(type) 处理不涉及任何变量的代码,比如处理来自外部或内部的数据格式,这时用变量就不合适了。

类型推断

只有当类型推断使不熟悉项目的读者更清楚地理解代码,或者使代码更安全时,才使用类型推断。不要仅仅为了避免编写显式类型的不便而使用它。

在某些上下文中,c++允许(甚至要求)编译器推导类型,而不是在代码中显式说明。

值得注意的是下列操作,某种程度上提高了效率。

1
2
// Sort `vec` in increasing order
std::sort(vec.begin(), vec.end(), [](auto lhs, auto rhs) { return lhs > rhs; });

类模板实参推断

暂时用不上

lambda表达式

在适当的地方使用lambda表达式。当lambda将逃离当前作用域时,更喜欢显式捕获。

Lambda表达式是创建匿名函数对象的一种简洁方法。当将函数作为参数传递时,它们通常很有用。例如:

1
2
3
std::sort(v.begin(), v.end(), [](int x, int y) {
return Weight(x) < Weight(y);
});

它们还允许通过名称显式地或使用默认捕获隐式地从封闭范围捕获变量。显式捕获要求列出每个变量,作为一个值或引用捕获:

1
2
3
4
5
6
int weight = 3;
int sum = 0;
// Captures `weight` by value and `sum` by reference.
std::for_each(v.begin(), v.end(), [weight, &sum](int x) {
sum += weight * x;
});

优点

传函数对象给 STL 算法,Lambdas 最简易,可读性也好。
Lambdas, std::functions 和 std::bind 可以搭配成通用回调机制(general purpose callback mechanism);写接收有界函数为参数的函数也很容易了。

缺点

Lambdas 的变量捕获略旁门左道,可能会造成悬空指针。
Lambdas 可能会失控;层层嵌套的匿名函数难以阅读。

结论

按 format 小用 lambda 表达式怡情。
禁用默认捕获,捕获都要显式写出来。打比方,比起 [=](int x) {return x + n;}, 您该写成 [n](int x) {return x + n;}才对,这样读者也好一眼看出 n 是被捕获的值。
匿名函数始终要简短,如果函数体超过了五行,那么还不如起名(acgtyrant 注:即把 lambda 表达式赋值给对象),或改用函数。
如果可读性更好,就显式写出 lambd 的尾置返回类型,就像auto.

模板编程

不要使用复杂的模板编程

定义

模板编程指的是利用c++ 模板实例化机制是图灵完备性, 可以被用来实现编译时刻的类型判断的一系列编程技巧

优点

模板编程能够实现非常灵活的类型安全的接口和极好的性能, 一些常见的工具比如Google Test, std::tuple, std::function 和 Boost.Spirit. 这些工具如果没有模板是实现不了的

缺点

  1. 模板编程所使用的技巧对于使用c++不是很熟练的人是比较晦涩, 难懂的. 在复杂的地方使用模板的代码让人更不容易读懂, 并且debug 和 维护起来都很麻烦
  2. 模板编程经常会导致编译出错的信息非常不友好: 在代码出错的时候, 即使这个接口非常的简单, 模板内部复杂的实现细节也会在出错信息显示. 导致这个编译出错信息看起来非常难以理解.
  3. 大量的使用模板编程接口会让重构工具(Visual Assist X, Refactor for C++等等)更难发挥用途. 首先模板的代码会在很多上下文里面扩展开来, 所以很难确认重构对所有的这些展开的代码有用, 其次有些重构工具只对已经做过模板类型替换的代码的AST 有用. 因此重构工具对这些模板实现的原始代码并不有效, 很难找出哪些需要重构.

结论

  1. 模板编程有时候能够实现更简洁更易用的接口, 但是更多的时候却适得其反. 因此模板编程最好只用在少量的基础组件, 基础数据结构上, 因为模板带来的额外的维护成本会被大量的使用给分担掉
  2. 在使用模板编程或者其他复杂的模板技巧的时候, 你一定要再三考虑一下. 考虑一下你们团队成员的平均水平是否能够读懂并且能够维护你写的模板代码.或者一个非c++ 程序员和一些只是在出错的时候偶尔看一下代码的人能够读懂这些错误信息或者能够跟踪函数的调用流程. 如果你使用递归的模板实例化, 或者类型列表, 或者元函数, 又或者表达式模板, 或者依赖SFINAE, 或者sizeof 的trick 手段来检查函数是否重载, 那么这说明你模板用的太多了, 这些模板太复杂了, 我们不推荐使用
  3. 如果你使用模板编程, 你必须考虑尽可能的把复杂度最小化, 并且尽量不要让模板对外暴漏. 你最好只在实现里面使用模板, 然后给用户暴露的接口里面并不使用模板, 这样能提高你的接口的可读性. 并且你应该在这些使用模板的代码上写尽可能详细的注释. 你的注释里面应该详细的包含这些代码是怎么用的, 这些模板生成出来的代码大概是什么样子的. 还需要额外注意在用户错误使用你的模板代码的时候需要输出更人性化的出错信息. 因为这些出错信息也是你的接口的一部分, 所以你的代码必须调整到这些错误信息在用户看起来应该是非常容易理解, 并且用户很容易知道如何修改这些错误

Boost

只使用 Boost 中被认可的库.

定义

Boost 库集 是一个广受欢迎, 经过同行鉴定, 免费开源的 C++ 库集.

优点

Boost代码质量普遍较高, 可移植性好, 填补了 C++ 标准库很多空白, 如型别的特性, 更完善的绑定器, 更好的智能指针。

缺点

某些 Boost 库提倡的编程实践可读性差, 比如元编程和其他高级模板技术, 以及过度 “函数化” 的编程风格.

结论

为了向阅读和维护代码的人员提供更好的可读性, 我们只允许使用 Boost 一部分经认可的特性子集. 目前允许使用以下库:

链接

std::hash

定义

hash<T>是c++ 11哈希容器用来哈希类型为T的键的函数对象,除非用户显式地指定了一个不同的哈希函数。例如,std::unordered_map<int, std::string>是一个散列映射,它使用std::hash<int>来散列它的键,而std::unordered_map<int, std::string, MyIntHash>使用MyIntHash。

hash是为所有整数、浮点数、指针和枚举类型以及一些标准库类型(如string和unique_ptr)定义的。用户可以为自己的类型定义专门化,从而使它能够为自己的类型工作。

hash很容易使用,并且简化了代码,因为不需要显式地命名它。专门化std::hash是指定如何对类型进行hash的标准方法,因此这是外部资源将提供的内容,也是新工程师所期望的内容。

std:hash很难专门化。它需要大量的样板代码,更重要的是,它将识别哈希输入的职责与执行哈希算法本身的职责结合起来。类型作者必须对前者负责,但后者需要类型作者通常不具备、也不应该需要的专业知识。这里的风险很高,因为由于哈希泛洪攻击的出现,低质量的哈希函数可能是安全漏洞。

其他C++特性

与Boost一样,一些现代c++扩展鼓励进行不利于可读性的编码实践,例如删除对读者有用的检查冗余(如类型名称),或者鼓励模板元编程。其他扩展通过现有机制复制可用的功能,这可能导致混淆和转换成本。

结论

以下C++特性不会用到

  1. 编译时的rational numbers (<ratio>),因为它与一个更类似模板的接口风格相关联。
  2. <cfenv><fenv>。因为许多编译器不完全支持这些特性。
  3. <filesystem>头文件,它没有足够的测试支持,并且存在固有的安全漏洞。

非标准扩展

除非另有说明,否则不能使用c++的非标准扩展。

定义

编译器支持标准c++之外的各种扩展。这些扩展包括GCC的_attribute__、内部函数(如_builtin_prefetch)、指定的初始化器(如Foo f = {.field = 3}),内联汇编,__counter____ pretty_function__,复合语句表达式(例如foo = ({ int x; Bar(&x); x },变长数组和alloca(),以及“Elvis操作符”a?:b

优点

  1. 非标准扩展可能提供标准c++中不存在的有用特性。例如,有些人认为指定的初始化器比标准的c++特性(如构造函数)更具可读性。
  2. 编译器的重要性能指南只能通过扩展来指定。

缺点

  1. 非标准扩展并不适用于所有编译器。使用非标准扩展会降低代码的可移植性。
  2. 即使在所有目标编译器中都支持扩展,扩展也常常没有很好地指定,而且编译器之间可能存在细微的行为差异。
  3. 非标准扩展添加到语言特性中,读者必须了解这些特性才能理解代码。

结论

可以使用使用非标准扩展实现的可移植性包装器,否则咱不用。

虚拟别名

公共别名是为了API的用户的利益,应该清楚地记录下来。

定义

创建别名的几种方法

1
2
3
typedef Foo Bar;
using Bar = Foo;
using other_namespace::Foo;

using替代typedef要更常见一些。可以和模板一些使用,以及更符合c++的语法。

头文件中定义的别名对于.cc文件同样具有效果,除非别名定义在函数中,类的私有部分中,或者具体标记的内部命名空间。

优点

  1. 别名可以通过简化长名称或复杂名称来提高可读性。
  2. 别名可以通过在一个地方命名API中重复使用的类型来减少重复,这可能使以后更改类型变得更容易。

缺点

  1. 如果将别名放置在客户端代码可以引用它们的头文件中,则会增加头文件API中的实体数量,从而增加其复杂性。
  2. 客户很容易依赖于公开别名的非预期细节,这使得更改非常困难。
  3. 创建一个仅用于实现的公共别名,而不考虑它对API或可维护性的影响,这很有吸引力。
  4. 别名会产生名称冲突的风险
  5. 别名可以通过给一个熟悉的结构一个不熟悉的名称来降低可读性
  6. 类型别名会创建不明确的API契约:不清楚别名是否保证与类型别名相同,是否具有相同的API,或者是否仅以特定的方式可用。

结论

不要在公用API中为了少打几个字去定义别名,只有当客户也需要使用此别名的时候才可以。

定义一个公用别名时,需要描述为何使用这个别名,是否保证此别名的有效性,兼容性。这让用户知道是否可以将类型视为可替换的,或者是否必须遵循更具体的规则,并可以帮助实现保留更改别名的某种程度的自由。

不要在公共API中使用命名空间别名

例如,这些别名记录了如何在客户端代码中使用它们:

1
2
3
4
5
6
7
8
namespace mynamespace {
// Used to store field measurements. DataPoint may change from Bar* to some internal type.
// Client code should treat it as an opaque pointer.
using DataPoint = foo::Bar*;

// A set of measurements. Just an alias for user convenience.
using TimeSeries = std::unordered_set<DataPoint, std::hash<DataPoint>, DataPointComparator>;
} // namespace mynamespace

这些别名没有记录预期的用途,其中有一半也不是为客户服务:

1
2
3
4
5
6
7
namespace mynamespace {
// Bad: none of these say how they should be used.
using DataPoint = foo::Bar*;
using std::unordered_set; // Bad: just for local convenience
using std::hash; // Bad: just for local convenience
typedef unordered_set<DataPoint, hash<DataPoint>, DataPointComparator> TimeSeries;
} // namespace mynamespace

但是,在函数定义、类的私有部分、显式标记的内部名称空间和.cc文件中,使用本地方便别名是很好的:

1
2
// In a .cc file
using foo::Bar;

命名

最重要的一致性规则是控制命名的规则。名称的样式立即通知我们命名实体是什么类型的:类型、变量、函数、常量、宏等等,而不需要搜索该实体的声明。我们大脑中的模式匹配引擎在很大程度上依赖于这些命名规则。
命名规则是相当随意的,但是我们认为一致性比这个领域的个人偏好更重要,所以不管你觉得它们是否合理,规则就是规则。

通用命名规则

使用即使对不同团队的人都清晰的名称来优化可读性。

命名应该描述对象的目的和意图,不要担心节省水平空间,因为让新读者立即理解您的代码要重要得多。尽量减少使用可能不为项目外部人员所知的缩写(尤其是缩写和首字母缩写)。不要删除一个单词中的字母来缩写。根据经验,如果在Wikipedia中列出了缩写,那么它可能是可以的。一般来说,描述性应该与名称的可见范围成正比。例如,n在这个5行的函数中可能是一个不错的名称,但是在类的范围内,它可能太模糊了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass {
public:
int CountFooErrors(const std::vector<Foo>& foos) {
int n = 0; // Clear meaning given limited scope and context
for (const auto& foo : foos) {
...
++n;
}
return n;
}
void DoSomethingImportant() {
std::string fqdn = ...; // Well-known abbreviation for Fully Qualified Domain Name
}
private:
const int kMaxAllowedConnections = ...; // Clear meaning within context
};

另一种极端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass {
public:
int CountFooErrors(const std::vector<Foo>& foos) {
int total_number_of_foo_errors = 0; // Overly verbose given limited scope and context
for (int foo_index = 0; foo_index < foos.size(); ++foo_index) { // Use idiomatic `i`
...
++total_number_of_foo_errors;
}
return total_number_of_foo_errors;
}
void DoSomethingImportant() {
int cstmr_id = ...; // Deletes internal letters
}
private:
const int kNum = ...; // Unclear meaning within broad scope
};

某些众所周知的缩写是可以的,比如迭代变量i和模板参数T。

出于以下命名规则的目的,“word”是您可以用英语编写的任何没有内部空格的单词。这包括缩写和首字母缩写;例如,对于每个单词的首字母都大写的“camel case”或“Pascal case”,使用StartRpc()这样的名称,而不是StartRPC()

模板参数应该遵循其类别的命名样式:类型模板参数应该遵循类型命名的规则,而非类型模板参数应该遵循变量命名的规则。

文件命名

文件名应该都是小写的,并且可以包含下划线(_)或破折号(-)。遵循项目使用的约定。如果没有一致的局部模式可遵循,则首选“_”。

可接受文件名的例子:

  • my_useful_class.cc
  • my-useful-class.cc
  • myusefulclass.cc
  • myusefulclass_test.cc // _unittest and _regtest are deprecated.

c++文件应该以.cc结尾,头文件应该以.h结尾。依赖于在特定点以文本形式包含的文件应该以.inc结尾(请参阅关于自包含头的部分)。

c++文件应该以.cc文件,.h文件,.inc文件做后缀

不要使用/usr/include中已经存在的文件名,比如db.h

一般来说,文件名需要具体详细,如http_server_logs.h,而不是logs.h,一种非常常见的情况是有一对名为foo_bar.hfoo_bar.cc的文件。定义一个名为FooBar的类。

类型命名

类型命名以大写字母开头,每个新单词都有大写字母,没有下划线:MyExcitingClass, MyExcitingEnum。

所有的class,structs,type alias,enums,type template parameters,都有相同的命名习惯。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map<UrlTableProperties *, std::string> PropertiesMap;

// using aliases
using PropertiesMap = hash_map<UrlTableProperties *, std::string>;

// enums
enum UrlTableErrors { ...

变量命名

变量名(包括函数参数)和数据成员都是小写的,单词之间有下划线。类(但不是结构)的数据成员还具有尾随下划线。例如:a_local_variablea_struct_data_membera_class_data_member_

  • 常见的变量命名
1
std::string table_name;  // OK - lowercase with underscore.
  • 类数据成员

类的数据成员,无论是静态的还是非静态的,都像普通的非成员变量一样命名,但是后面有一个下划线。

1
2
3
4
5
6
class TableInfo {
...
private:
std::string table_name_; // OK - underscore at end.
static Pool<TableInfo>* pool_; // OK.
};
  • 结构体数据成员

结构的数据成员,包括静态和非静态的,都像普通的非成员变量一样命名。它们没有类中的数据成员所具有的尾随下划线。

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

常量命名

声明了constexprconst的变量,其值在程序执行期间是固定的,其名称以“k”开头,后跟混合大小写。下划线可用于分隔符,在大小写不能用于分隔的罕见情况下。例如:

1
2
const int kDaysInAWeek = 7;
const int kAndroid8_0_0 = 24; // Android 8.0.0

所有具有静态存储持续时间的变量(即静态变量和全局变量,详细信息请参阅存储持续时间)都应该这样命名。此约定对于其他存储类的变量(例如自动变量)是可选的,否则将应用通常的变量命名规则。

函数命名

一般函数有混合情况;访问器和修改器可以像变量一样命名。
通常,函数应该以大写字母开头,并且每个新单词都有大写字母。

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

(同样的命名规则也适用于作为API的一部分公开的类和名称空间范围常量,这些常量看起来像函数,因为它们是对象而不是函数,这是一个不重要的实现细节。)

访问器和修改器(get和set函数)可以像变量一样命名。这些通常对应于实际的成员变量,但这不是必需的。例如,int count()void set_count(int count)

命名空间的命名

名称空间名称都是小写的。顶级名称空间名称基于项目名称。避免嵌套名称空间与知名顶级名称空间之间的冲突

顶级名称空间的名称通常应该是其代码包含在该名称空间中的项目或团队的名称。该名称空间中的代码通常应该位于一个目录中,该目录的basename与名称空间名称匹配(或在其子目录中匹配)。

请记住,针对缩写名称的规则与适用于变量名称的规则一样适用于名称空间。名称空间中的代码很少需要提及名称空间名称,所以通常不需要特别使用缩写。

避免与知名顶级名称空间匹配的嵌套名称空间。由于名称查找规则,名称空间名称之间的冲突可能导致意外的构建中断。特别是,不要创建任何嵌套的std名称空间。优先选择唯一的项目标识符(websearch::index, websearch::index_util),而不是容易发生冲突的名称,如websearch::util

对于内部名称空间,要注意添加到相同内部名称空间中的其他代码是否会导致冲突(团队中的内部帮助程序往往是相关的,可能会导致冲突)。在这种情况下,使用文件名创建惟一的内部名称是有帮助的(websearch::index::frobber_internal用于frobber.h)

枚举命名

枚举器(适用于作用域枚举和非作用域枚举)应该像常量或宏一样命名: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,
};

宏命名

一般来说不用宏,无法避免的时候使用字母大写以及下划线:

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

命名特列

如果要命名类似于现有C或c++实体的东西,那么可以遵循现有的命名约定方案。

比如

bigopen(): 函数名, 参照 open() 的形式

uint: typedef

bigpos: structclass, 参照 pos 的形式

sparse_hash_map: STL 型实体; 参照 STL 命名约定

LONGLONG_MAX: 常量, 如同 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 {
...
};

函数注释

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

函数声明

基本上每个函数声明处前都应当加上注释, 描述函数的功能和用途. 只有在函数的功能简单而明显时才能省略这些注释(例如, 简单的取值和设值函数). 注释使用叙述式 (“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();

变量注释

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

  • 类数据成员

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

特别地, 如果变量可以接受 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. */
  • 函数参数注释

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

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

比如下面的示例的对比:

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.”).

格式规定

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

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

行长

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

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

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

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

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

非ASCII字符

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

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

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

(Yang.Y 注: "\xEF\xBB\xBF" 通常用作 UTF-8 with BOM 编码标记)

使用 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) {}

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())

浮点常量

浮点文字应该始终有一个基数点,两边都有数字,即使它们使用指数表示法。如果所有浮点文字都采用这种熟悉的形式,可读性将得到提高,因为这有助于确保它们不会被误认为整数文字,并且指数表示法的e/e不会被误认为十六进制数字。用整型文字初始化浮点变量是可以的(假设变量类型可以精确地表示该整数),但请注意,指数表示法中的数字永远不是整型文字。

1
2
3
float f = 1.f; //bad
long double ld = -.5L; //bad
double d = 1248e6; //bad
1
2
3
4
float f = 1.0f;
float f2 = 1; // Also OK
long double ld = -0.5L;
double d = 1248.0e6;

函数调用

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

函数调用遵循如下形式:

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 和左圆括号间都有个空格. 右圆括号和左大括号之间也要有个空格:

循环和开关语句

和if一样,()两边空格,然后花括号要开。

指针和引用表达

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

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

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) {
...
}

不要用词语形式的 andcompl.

返回值

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

变量和数组初始化

=, (){} 均可.

注意以下操作:

1
2
3
4
vector<int> v(100, 1);  // 内容为 100 个 1 的向量.
vector<int> v{100, 1}; // 内容为 100 和 1 的向量.
int pi(3.14); // 好 - pi == 3.
int pi{3.14}; // 编译错误: 缩窄转换.即用{}更严谨一点。

预处理指令

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

一般来说格式化模板也是这么进行格式化的。

类格式

访问控制块的声明依次序是 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.
  • 关于声明顺序的规则请参考 声明顺序 一节.

构造函数初始化列表

show you the code.

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() { // 正确. 命名空间内没有额外的缩进.
...
}

} // n

不要在命名空间内缩进:

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; } // 用括号把大括号与实现分开.
...

添加冗余的留白会给其他人编辑时造成额外负担. 因此, 行尾不要留空格. 如果确定一行代码已经修改完毕, 将多余的空格去掉; 或者在专门清理空格时去掉(尤其是在没有其他人在处理这件事的时候). (Yang.Y 注: 现在大部分代码编辑器稍加设置后, 都支持自动删除行首/行尾空格, 如果不支持, 考虑换一款编辑器或 IDE)

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
14
// 赋值运算符前后总是有空格.
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 编码风格, 这儿有必要重申一下某些你可能会忘记的指南:

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

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

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

结束语

运用常识和判断力, 并且 保持一致.

编辑代码时, 花点时间看看项目中的其它代码, 并熟悉其风格. 如果其它代码中 if 语句使用空格, 那么你也要使用. 如果其中的注释用星号 (*) 围成一个盒子状, 那么你同样要这么做.

风格指南的重点在于提供一个通用的编程规范, 这样大家可以把精力集中在实现内容而不是表现形式上. 我们展示的是一个总体的的风格规范, 但局部风格也很重要, 如果你在一个文件中新加的代码和原有代码风格相去甚远, 这就破坏了文件本身的整体美观, 也让打乱读者在阅读代码时的节奏, 所以要尽量避免.

好了, 关于编码风格写的够多了; 代码本身才更有趣. 尽情享受吧!