谷歌 C++ 风格指南
背景
C++是谷歌许多开源项目使用的主要开发语言之一。 正如每个C++程序员都知道的,该语言具有许多强大的功能,但这种功能带来了复杂性,这反过来又会使代码更容易出错,更难阅读和维护。
本指南的目标是通过详细描述编写 C++ 代码的注意事项来驾驭这种复杂性。 这些规则的存在是为了保持代码库的可管理性,同时仍然允许编码人员有效地使用 C++ 语言特性。
样式,也称为可读性,我们称之为规范我们 C++ 代码的约定。 术语“风格”有点用词不当,因为这些约定不仅仅涵盖源代码的格式。
Google 开发的大多数开源项目都符合本指南的要求。
请注意,本指南不是C++教程:我们假设读者熟悉该语言。
这份风格指南的目标
为什么我们有这个文件?
我们认为本指南应该服务于几个核心目标。 它们是每条规则的基本原因。 通过提出这些想法,我们希望能够进行讨论,使我们更广泛的社区更清楚地了解为什么制定这些规则,以及某些特别的决定是因为什么而制定。 如果你了解每个规则服务于哪些目标,那么当可以放弃一条规则时(有些可以),以及更改指南中的规则需要什么样的参数或替代方法,每个人都应该更清楚。
风格指南的目标如下:
- 风格规则应发挥作用
- 风格规则的好处必须足够大,足以证明要求我们所有的工程师记住它是合理的。 收益是相对于没有规则的代码库来衡量的,因此,如果人们无论如何都不可能这样做,那么针对非常有害做法的规则可能仍然具有很小的好处。 这一原则主要解释了我们没有的规则,而不是我们有的规则:例如
goto
违反了以下许多原则,但已经很少见了,所以《风格指南》不讨论它。 - 为阅读人员而不是编写人员优化
- 我们的代码库(以及提交给它的大多数单个组件)预计会持续相当长一段时间。 因此,阅读我们的大部分代码的时间将比编写代码的时间更多。 我们明确选择优化,以优化我们的平均软件工程师在代码库中读取、维护和调试代码的体验,而不是在编写上述代码时轻松。 "为读者留下痕迹"是此原则的一个特别常见的子点:当代码段中发生意外或异常时(例如指针所有权的转移),在使用点为读者留下文本提示是有价值的(
std::unique_ptr
在调用点上明确显示所有权转移)。 - 与现有代码保持一致
- 通过我们的代码库始终使用一种样式,使我们能够专注于其他(更重要的)问题。 一致性还允许自动化:格式化代码或调整
#include
的工具只有在代码与工具的期望一致时才能正常工作。 在许多情况下,被归结为"保持一致"的规则归结为"只挑一个,不再担心它";让人们争论这些观点的成本超过了在这些点上允许灵活性的潜在价值。 - 在适当的时候与更广泛的C++社区保持一致
- 与其他组织使用C++的方式保持一致,其价值与代码库中的一致性相同。 如果标准中C++功能解决了问题,或者如果某些习惯用法广为人知和被接受,这就是使用它的论据。 但是,有时标准功能和习惯用法存在缺陷,或者只是在设计时没有考虑到我们的代码库的需求。 在这些情况下(如下所述),约束或禁止标准功能是合适的。 在某些情况下,我们更喜欢自产或第三方库,而不是C++标准中定义的库,这要么是出于感知的优越性,或者没有足够的价值将代码库转换为标准接口。
- 避免意外或危险的构造
- C++具有比人们想象的更令人惊讶或更危险的功能。 一些风格指南限制是到位的,以防止落入这些陷阱。 对于这类具有限制的风格指南,放弃它们的门槛很高,因为放弃这样的规则通常会直接危害程序的正确性。
- 避免我们的平均C++程序员会觉得棘手或难以维护的构造
- C++ 具有不太广泛适用性的特性,因为它们在代码中引入了复杂性。 在广泛使用的代码中,使用更棘手的语言构造可能更容易被接受,因为更复杂的实现的任何好处都会被使用广泛地乘以,并且在使用新的新代码时不需要再次支付理解复杂性的成本部分代码库。 如有疑问,可以通过询问您的项目线索来寻求对此类规则的豁免。 这对我们的代码库尤其重要,因为代码所有权和团队成员会随时间而变化:即使使用某些代码段的每个人都目前都理解它,这种理解也不能保证在几年后保持。
- 注意我们的规模
- 拥有 1 亿多行代码库和数千名工程师,一个工程师的错误和简化对于许多人来说可能变得成本高昂。 例如,避免污染全局命名空间尤为重要:在数亿行的代码库中发生名称冲突是难以处理的,如果每个人都将内容放入全局命名空间,则很难避免。
- 必要时允许优化
- 性能优化有时可能是必要和适当的,即使它们与本文档的其他原则相冲突也是如此。
本文档的目的是提供具有合理限制的最大指导。 和往常一样,常识和良好的品味应该占上风。 因此,我们特意参考整个 Google C++ 社区已建立好的惯例,而不仅仅是你的个人偏好或你的团队偏好。 对于看上去聪明和不常见的结构要多多怀疑并谨慎使用:没有禁止不等于可以使用。 运用你的判断,如果你不确定,请不要犹豫,询问你的项目领导们的意见。
C++ 的版本
目前,代码应针对C++17,即不应使用 C++2x 的特性。 本指南所针对的 C++ 版本将随着时间的推移而(积极)前进。
不要使用非标准扩展。
头文件
通常,每个.cc
文件都应具有关联的.h
文件。 有一些情况属于例外,例如单元测试和仅包含一个main()
函数的小的.cc
文件。
正确使用头文件会对代码的可读性、大小和性能产生巨大影响。
以下规则将指导你规避使用头文件的各种陷阱。
自包含的头文件
头文件应该是自包含的(可以自行编译)并且以 .h
结尾。 用于包含但不是头文件的文件应该以 .inc
结尾并谨慎使用。
所有头文件都应该是自包含的。 用户和重构工具不应该需要遵循特殊条件才能包含相关的头文件。 具体而言,一个头文件应具有头文件保护并包含所需的所有其他头文件。
最好将模板和内联函数的定义与其声明放在同一文件中。 这些构造的定义必须包含在使用它们的每个.cc
文件中,否则程序可能无法在某些生成配置中链接。 如果声明和定义在不同的文件中,则包含前者应可传递地包含后者。 不要将这些定义移动到单独包含的头文件(-inl.h
);这种做法在过去很常见,但不再被允许。
作为一个特例,如果一个模板为相关的所有模板参数集显式实例化或者该模板是类的私有详细实现,则允许在实例化这个模板的有且仅有的.cc
文件中定义该模板。
在极少数情况下,included 的文件故意设计成不是自包含的。 这些文件通常期望在不寻常的位置被 included,如另一个文件的中间。 它们可能不使用头文件保护,并且可能不包含其先决条件。 使用.inc
扩展名命名此类文件。 请谨慎使用,并尽可能使用自包含头文件。
#define 保护
所有头文件都应具有 #define
保护,以防止多重包含。 名称符号的格式应该是
<PROJECT>_<PATH>_<FILE>_H_
。
为了保证唯一性,它们应该基于项目源代码树中的完整路径。 例如,项目foo
中的文件foo/src/bar/baz.h
应具有以下保护:
#ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif // FOO_BAR_BAZ_H_
前置声明
尽可能避免使用前置声明。
而是 #include
所需的头文件。
"前置声明"是类、函数或模板的声明,没有与之相关联的定义。
- 前置声明可以节省编译时间,因为
#include
强制编译器打开更多文件并处理更多输入。 - 前置声明可以节省不必要的重新编译。 由于头文件中的不相关更改
#include
可能强制更频繁地重新编译代码。
- 前置声明可能隐藏依赖项,从而允许用户代码在头文件更改时忽略必要的重新编译。
- 对库的后续更改可能会破坏前置声明。 函数和模板的前置声明可能让头文件所有者无法对其 API 进行更广泛兼容性的更改,例如扩大参数类型、添加具有默认值的模板参数或迁移到新的命名空间。
- 前置声明来自命名空间
std::
的符号产生未定义的行为。 - 可能很难确定是需要前置声明还是完全
#include
。 用前置声明代替#include
可能会默默地改变代码的含义:// 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
该头文件。 - 使用类模板时,最好
#include
其头文件。
有关何时 #include 头文件的规则,请参阅include 的名称和顺序。
内联函数
仅当函数较小(例如 10 行或更少)时,才内联定义函数。
当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用。
只要内联函数很小,内联函数就可以生成更高效的对象代码。 对于访问函数和赋值函数以及其它简短、 性能重要的函数, 请自由使用内联。
过度使用内联实际上会使程序变慢。 根据函数的大小,内联它可能导致代码大小增加或减小。 内联一个非常小的访问函数通常会减小代码大小,而内联非常大的函数可以显著增加代码大小。 在现代处理器上,由于更好地使用指令缓存,较小的代码通常运行得更快。
一个不错的经验法则是,如果函数超过 10 行长,则不内联函数。 小心析构函数,由于隐式成员和基析构函数调用,析构函数通常比它们出现的时间长!
另一个有用的经验法则:使用循环或 switch 语句内联函数通常不经济有效(除非在通常情况下,循环或 switch 语句永远不会执行)。
请务必知道,即使函数声明为内联,也并不总是内联的。例如,虚拟函数和递归函数通常不内联。 通常递归函数不应是内联的。 使虚拟函数内联的主要原因是将其定义放在类中,以便方便或记录其行为,例如访问器和突变器。
include 的名称和顺序
按以下顺序 include:关联的头文件、C 系统头文件、C++ 标准库头文件、其它库的头文件、项目头文件。
一个项目的所有头文件都应以该项目的源代码目录开始,不要使用 UNIX 目录别名.
(当前目录)或 ..
(父目录)。 例如应该这样 include google-awesome-project/src/base/logging.h
:
#include "base/logging.h"
假设 dir/foo.cc
和 dir/foo_test.cc
主要目的分别是实现和测试 dir2/foo2.h
,请按如下顺序 include:
dir2/foo2.h
。- 一个空行
- C 系统头文件(更精确的描述:尖括号中带有
.h
扩展名的头文件),例如<unistd.h>
,<stdlib.h>
。 - 一个空行
- C++ 标准库标头文件(不带文件扩展名),例如
<algorithm>
,<cstddef>
。 - 一个空行
- 其它库的
.h
文件。 -
项目的
.h
文件。
将每个非空组用一个空行分隔。
使用这种排序,如果头文件 dir2/foo2.h
遗漏任何必需的 include,则 dir/foo.cc
或 dir/foo_test.cc
的构建会立刻中断。
因此这一条规则保证维护这些文件的人首先看到构建中止的消息而不是维护其它包的不相干的人。
dir/foo.cc
和 dir2/foo2.h
通常位于同一目录中(例如base/basictypes_test.cc
和 base/basictypes.h
),但有时也可能位于不同的目录中。
请注意,C 的头文件如 stddef.h
基本上可以与对应的 C++ 头文件(cstddef
)互换。
这两种风格都是可以接受的,但更倾向与现有代码保持一致。
在每个部分中,include 应按字母顺序排序。 请注意,较旧的代码可能不符合此规则,应在方便时修复。
应 include 定义所依赖的符号的所有头文件,除非是前置声明中的特殊情况。 如果你依赖于 bar.h
中的符号,则不要指望 foo.h
(此刻)include bar.h
:你要自己 include bar.h
,除非 foo.h
的意图明确表明其为你提供 bar.h
中的符号。
例如,google-awesome-project/src/foo/internal/fooserver.cc
中的 include 可能如下所示:
#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"
特例:
有时,与系统相关的代码需要条件 include。 此类代码可以将条件 include 放在其它 include 之后。 当然,要保持与系统相关的代码较小且集中。 例子:
#include "foo/public/fooserver.h" #include "base/port.h" // For LANG_CXX11. #ifdef LANG_CXX11 #include <initializer_list> #endif // LANG_CXX11
作用域
命名空间
除少数特例,请将代码放在命名空间中。 命名空间的名称应唯一,且基于项目的名称和路径。 不要使用 using 指令(例如 using namespace foo
)。 不要使用内联命名空间。 有关匿名命名空间,请参阅匿名命名空间和静态变量。
命名空间将全局作用域细分为不同的命名作用域,因此对于防止全局作用域中的名称冲突非常有用。
命名空间提供了一种防止大型程序中的名称冲突的方法,同时允许大多数代码使用合理的短名称。
例如,如果两个不同的项目在全局作用域中有一个类Foo,
则这些符号可能在编译时或运行时发生冲突。 如果每个项目将其代码放在一个命名空间中,那么 project1::Foo
和 project2::Foo
是不会发生冲突的不同符号,并且每个项目的命名空间中的代码可以继续引用 Foo
而无需前缀。
内联命名空间会自动将其名称放在封闭作用域中。 请考虑以下代码段,例如:
namespace outer { inline namespace inner { void foo(); } // namespace inner } // namespace outer
表达式 outer::inner::foo()
和 outer::foo()
可互换。 内联命名空间主要用于跨版本的 ABI 兼容性。
命名空间可能会令人困惑,因为它们使找出名称引用的定义的机制复杂化。
特别是,内联命名空间可能会令人困惑,因为名称实际上并不局限于声明它们的命名空间。 它们仅作为某些较大版本控制策略的一部分有用。
在某些情况下,有必要通过符号的完全限定名称重复引用符号。 对于深度嵌套的命名空间,这会增加大量杂乱。
命名空间应使用如下:
- 遵循命名空间命名的规则。
- 命名空间后面需带有注释,如给定示例所示。
-
在 include、gflags定义/声明和类的前置声明之后,用命名空间封装整个源代码文件以区别其它命名空间。
// In the .h file namespace mynamespace { // All declarations are within the namespace scope. // Notice the lack of indentation. class MyClass { public: ... void Foo(); }; } // namespace mynamespace
// In the .cc file namespace mynamespace { // Definition of functions is within scope of the namespace. void MyClass::Foo() { ... } } // namespace mynamespace
更复杂的
.cc
文件可能具有其它细节,如 flags 或 using 声明。#include "a.h" ABSL_FLAG(bool, someflag, false, "dummy flag"); namespace mynamespace { using ::foo::Bar; ...code for mynamespace... // Code goes against the left margin. } // namespace mynamespace
- 若要将生成的协议消息代码放在命名空间中,请使用
.proto
文件中的package
指定符。 有关详细信息,请参阅协议缓冲区包。 - 不要在命名空间
std
中声明任何内容,包括标准库类的正向声明。 在命名空间std
中声明实体是未定义的行为,即不可移植。 要从标准库中声明实体,请包括相应的头文件。 不得使用 using 指令 使命名空间中的所有名称都可访问。
// Forbidden -- This pollutes the namespace. using namespace foo;
不要在头文件中使用命名空间别名除非显式标记为仅内部使用的命名空间,因为在头文件中引入到命名空间的任何内容都会成为公开 API 的一部分。
// Shorten access to some commonly used names in .cc files. namespace baz = ::foo::bar::baz;
// Shorten access to some commonly used names (in a .h file). namespace librarian { namespace impl { // Internal, not part of the API. namespace sidetable = ::pipeline_diagnostics::sidetable; } // namespace impl inline void my_inline_function() { // namespace alias local to a function (or method). namespace baz = ::foo::bar::baz; ... } } // namespace librarian
- 不要使用内联命名空间。
匿名命名空间和静态变量
当 .cc
文件中的定义不需要在该文件外部引用时,将它们放在未命名的命名空间中或声明它们为 static
。 请勿在 .h
文件中使用这些构造。
通过将所有声明放在未命名的命名空间中,可以给出内部链接。 函数和变量也可以通过声明它们为 static
来拥有内部链接性。 这意味着你声明的任何内容都无法从另一个文件访问。 如果其他文件声明了具有相同名称的内容,则两个实体是完全独立的。
对于不需要在其他地方引用的所有代码,建议在.cc
文件中使用内部链接。
请勿在.h
文件中使用内部链接。
匿名命名空间和具名命名空间格式相同。 在最后的注释中,将命名空间名称留空:
namespace { ... } // namespace
非成员函数、静态成员函数和全局函数
最好将非成员函数放在名称空间中;少用完全全局的函数。 不要使用类来仅仅分组静态函数。 类的静态方法通常应与类的实例或类的静态数据密切相关。
在某些情况下,非成员函数和静态成员函数可能很有用。 将非成员函数放在命名空间中可避免污染全局命名空间。
非成员函数和静态成员函数作为新类的成员可能更有意义,尤其是当它们访问外部资源或具有显著的依赖项时。
有时,定义不绑定到类实例的函数很有用。 此类函数可以是静态成员函数,也可以是非成员函数。 非成员函数不应依赖于外部变量,并且几乎应始终存在于命名空间中。 不要创建类来仅仅对静态成员函数进行分组;这与为函数名称提供公共前缀没有什么不同,而且这种分组通常无论如何都没有必要。
如果定义非成员函数,并且仅需要其.cc
文件中的函数,请使用内部链接来限制其范围。
局部变量
将函数变量尽可能放在最小作用域内, 并在声明时初始化变量。
C++ 允许你在函数中的任意位置声明变量。 我们建议在尽可能小的作用域中声明变量, 离第一次使用越近越好。 这使读者更容易找到声明,并查看变量的类型以及初始化到什么类型。 特别是,应使用初始化而不是声明和再赋值,例如:
int i;
i = f(); // Bad -- initialization separate from declaration.
int j = g(); // Good -- declaration has initialization.
std::vector<int> v; v.push_back(1); // Prefer initializing using brace initialization. v.push_back(2);
std::vector<int> v = {1, 2}; // Good -- v starts initialized.
if
和
for
语句所需的变量通常应在这些语句中声明,以便这些变量仅限于这些作用域。 例如:
while (const char* p = strchr(str, '/')) str = p + 1;
有一个警告:如果变量是一个对象,则每次它进入作用域并创建时都会调用其构造函数,并且每次超出作用域时都会调用其析构函数。
// Inefficient implementation: 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); }
静态和全局变量
具有静态存储持续时间的对象是被禁止的,除非它们可轻微地可破坏。 非正式地说,这意味着析构函数不会执行任何操作,甚至不考虑成员和基析构函数。 更正式地说,这意味着该类型没有用户定义或虚拟析构函数,并且所有基和非静态成员都是可析构的。 静态函数局部变量可以使用动态初始化。 不建议在命名空间作用域对静态类成员变量或变量使用动态初始化,但在有限的情况下允许使用;有关详细信息,请参阅下文。
作为经验法则:如果全局变量的单独声明可以是constexpr,
则满足这些要求。
每个对象都有一个存储持续时间,这与它的生存期相关。 具有静态存储持续时间的对象从初始化到程序结束一直直播。 此类对象在命名空间作用域("全局变量")中显示为变量,作为类的静态数据成员,或作为使用静态
指定符声明的函数-局部变量。 当控件首次通过其声明时,将初始化函数本地静态变量;具有静态存储持续时间的所有其他对象将作为程序启动的一部分初始化。 具有静态存储持续时间的所有对象在程序退出时销毁(在终止未联接的线程之前发生)。
初始化可能是动态的,这意味着在初始化期间发生了一些非同寻常的事情。 (例如,考虑分配内存的构造函数,或使用当前进程 ID 初始化的变量。 另一种初始化是静态初始化。 不过,两者并不完全相反:静态初始化总是发生在具有静态存储持续时间的对象(将对象初始化为给定的常量或由设置为零的所有字节组成的表示形式)时,而动态初始化则发生于此之后(如果需要)。
全局变量和静态变量对于大量应用程序非常有用:命名常量、某些转换单元内部的辅助数据结构、命令行标志、日志记录、注册机制、后台基础结构等。
使用动态初始化或具有非简单析构函数的全局变量和静态变量会产生复杂性,很容易导致难以查找的 Bug。 动态初始化不跨翻译单元排序,销毁也不会排序(除了销毁以初始化相反的顺序发生)。 当一个初始化引用另一个具有静态存储持续时间的变量时,这有可能导致对象在其生存期开始之前(或其生存期结束之后)被访问。 此外,当程序启动未在退出时联接的线程时,如果对象的生存期已结束,则这些线程可能会尝试访问对象。
关于析构的决定
当析构函数是琐碎的时,它们的执行根本不受排序(它们实际上不是"运行");否则,我们将面临在对象生存期结束后访问对象的风险。 因此,我们只允许具有静态存储持续时间的对象,如果它们可简单破坏。
基本类型(如指针和int)
是可简单破坏的,而可简单可破坏类型的数组也是可破坏的数组。 请注意,标有constexpr
的变量是可简单破坏的。
const int kNum = 10; // allowed struct X { int n; }; const X kX[] = {{1}, {2}, {3}}; // allowed void foo() { static const char* const kMessages[] = {"hello", "world"}; // allowed } // allowed: constexpr guarantees trivial destructor constexpr std::array<int, 3> kArray = {{1, 2, 3}};
// bad: non-trivial destructor const std::string kFoo = "foo"; // bad for the same reason, even though kBar is a reference (the // rule also applies to lifetime-extended temporary objects) const std::string& kBar = StrCat("a", "b", "c"); void bar() { // bad: non-trivial destructor static std::map<int, int> kData = {{1, 0}, {2, 0}, {3, 0}}; }
请注意,引用不是对象,因此它们不受破坏性约束。 但是,动态初始化的约束仍然适用。 特别地,允许 static T& t = *new T;
形式的函数局部引用。
关于初始化的决定
初始化是一个更复杂的主题。 这是因为我们不仅要考虑类构造函数是否执行,而且还必须考虑初始化器的评估:
int n = 5; // fine int m = f(); // ? (depends on f) Foo x; // ? (depends on Foo::Foo) Bar y = g(); // ? (depends on g and on Bar::Bar)
除第一个语句外,所有语句都让我们面临不确定的初始化顺序。
我们正在寻找的概念是C++标准的正式语言中称为不断初始化。 这意味着初始化表达式是常量表达式,如果该对象由构造函数调用初始化,则必须将构造函数指定为constexpr:
struct Foo { constexpr Foo(int) {} }; int n = 5; // fine, 5 is a constant expression Foo x(2); // fine, 2 is a constant expression and the chosen constructor is constexpr Foo a[] = { Foo(1), Foo(2), Foo(3) }; // fine
始终允许恒定初始化。 静态存储持续时间变量的恒定初始化应用constexpr
标记,或在可能的情况下标记
ABSL_CONST_INIT
属性。 任何未如此标记的非本地静态存储持续时间变量都应假定具有动态初始化,并非常仔细地审查。
相比之下,以下初始化存在问题:
// Some declarations used below. time_t time(time_t*); // not constexpr! int f(); // not constexpr! struct Bar { Bar() {} }; // Problematic initializations. time_t m = time(nullptr); // initializing expression not a constant expression Foo y(f()); // ditto Bar b; // chosen constructor Bar::Bar() not constexpr
不鼓励非局部变量的动态初始化,一般禁止。 但是,如果程序没有任何方面依赖于此初始化相对于所有其他初始化的顺序,我们确实允许这样做。 根据这些限制,初始化的顺序不会产生可观察到的差异。 例如:
int p = getpid(); // allowed, as long as no other static variable // uses p in its own initialization
允许静态局部变量的动态初始化(和通用)。
常见模式
- 全局字符串:如果需要全局或静态字符串常量,请考虑使用简单字符数组或指向字符串文本的第一个元素的字符指针。 字符串文本已具有静态存储持续时间,通常就足够了。
- 映射、集和其他动态容器:如果需要静态固定集合(如要搜索的集或查找表),则不能将标准库中的动态容器用作静态变量,因为它们具有不平凡的析构函数。 相反,请考虑一个简单的简单类型数组,例如 int 数组(对于"从 int 到 int 的映射"),或对数组(例如
int
和const char*
对)。 对于小型集合,线性搜索是完全足够的(并且由于内存局部性而高效);考虑使用absl/算法/容器.h中的设施进行标准操作。 如有必要,请按排序顺序保存集合并使用二进制搜索算法。 如果确实喜欢标准库中的动态容器,请考虑使用函数本地静态指针,如下所述。 - 智能指针 (
unique_ptr,
shared_ptr
) 智能指针在销毁期间执行清理, 因此是被禁止的. 请考虑您的用例是否适合本节中描述的其他模式之一。 一个简单的解决方案是使用指向动态分配对象的纯指针,并且从不删除它(请参阅最后一项)。 - 自定义类型的静态变量:如果需要需要自己定义的静态常量数据,请为类型提供一个普通析构函数和一个
constexpr
构造函数。 - 如果所有其他操作都失败,则可以动态创建对象,并且永远不会使用函数本地静态指针或引用(例如
static const auto& impl = *new T(args...);
)。
thread_local 变量
thread_local
函数内未声明的变量必须使用真正的编译时常量初始化,并且必须使用
ABSL_CONST_INIT
属性。 thread_local
首选,而不是定义线程本地数据的其他方法。
从 C++11 开始,可以使用thread_local
指定符声明变量:
thread_local Foo foo = ...;
此类变量实际上是对象的集合,因此当不同的线程访问它时,它们实际上是访问不同的对象。
thread_local
变量在许多方面与静态存储持续时间变量十分类似。 例如,它们可以在命名空间作用域、函数内部或静态类成员中声明,但不能作为普通类成员声明。
thread_local
变量实例的初始化与静态变量非常类似,只不过每个线程必须单独初始化它们,而不是在程序启动时初始化一次。 这意味着在函数中声明的thread_local
变量是安全的,但其他thread_local
变量受到与静态变量相同的初始化顺序问题(以及更多)。
thread_local
变量实例在其线程终止时被销毁,因此它们没有静态变量的销毁顺序问题。
- 线程本地数据本质上是不受争用的(因为通常只有一个线程可以访问它),这使得
thread_local
对并发编程很有用。 thread_local
是创建线程本地数据的唯一标准支持的方法。
- 访问
thread_local
变量可能会触发执行不可预测且无法控制的其他代码量。 thread_local
变量实际上是全局变量,除了缺少线程安全性之外,还有全局变量的所有缺点。thread_local
变量消耗的内存会随正在运行的线程数(在最坏的情况下)而缩放,这在程序中可能相当大。- 普通班员不能
thread_local。
thread_local
可能不如某些编译器内部函数有效。
函数中的thread_local
变量没有安全问题,因此可以不受限制地使用。 请注意,可以使用函数范围thread_local
通过定义公开它的函数或静态方法来模拟类或命名空间范围thread_local:
Foo& MyThreadLocalFoo() { thread_local Foo result = ComplicatedInitialization(); return result; }
类或命名空间作用域thread_local变量必须
使用真正的编译时常量初始化(即它们必须没有动态初始化)。 为了强制实施,thread_local
类或命名空间作用域的变量必须用
ABSL_CONST_INIT
(或康斯普,
但这应该是罕见的):
ABSL_CONST_INIT thread_local Foo foo = ...;
thread_local
应该优先于用于定义线程本地数据的其他机制。
类
类是 C++ 代码的基本单元。 自然,我们广泛使用它们。 本节列出编写类时应遵循的主要注意事项。
构造函数的职责
避免在构造函数中调用虚函数,并避免在无法报出错误时进行可能失败的初始化。
可以在构造函数的正文中执行任意初始化。
- 无需担心类是否已初始化。
- 通过构造函数调用完全初始化的对象可以是
const
的,并且可能更易于与标准容器或算法一起使用。
- 如果调用虚函数,在子类的实现中将不会获得这些调用。 将来对类的修改可能悄悄地引入此问题造成很大混乱,即使你的类当前没有子类化实现。
- 在没有使程序崩溃 (因为并不是一个始终合适的方法) 或者使用异常 (因为已经被禁用) 等方法的条件下, 构造函数很难上报错误。
- 如果执行失败, 会得到一个初始化失败的对象, 这个对象有可能进入不正常的状态, 必须使用
bool IsValid()
(或类似的)机制才能检查出来, 然而这是一个十分容易被疏忽的方法。 - 构造函数的地址获取不到,因此在构造函数中完成的任何工作都无法以简单的方式转交,例如转交给另一个线程。
构造函数不应调用虚拟函数。 如果适合你的代码 ,终止程序可能是合适的错误处理响应。 否则,请考虑TotW #42中所述的工厂函数或 Init()
方法。
避免使用 Init()
方法的对象,而对象不会影响可以调用哪些公共方法(此形式的半构造对象特别难以正确处理)。
隐式转换
不要定义隐式转换。 对转换运算符和单参数构造函数使用显式
关键字。
隐式转换允许一种类型(称为源类型)的对象用于需要不同类型(称为目标类型)的位置,例如将int
参数传递给采用双精度
参数的函数时。
除了由语言定义的隐式转换外,用户还可以通过将适当的成员添加到源或目标类型的类定义来定义自己的。 源类型的隐式转换由以目标类型命名的类型转换运算符定义(例如operator bool()
)。 目标类型中的隐式转换由构造函数定义,该构造函数可以将源类型作为其唯一的参数(或仅具有默认值的参数)。
显式
关键字可以应用于构造函数或(自 C++11 起)转换运算符,以确保它只能在目标类型在使用点(例如使用强制转换)时显式时使用。 这不仅适用于隐式转换,也适用于C++11 的列表初始化语法:
class Foo { explicit Foo(int x, double y); ... }; void Func(Foo f);
Func({42, 3.14}); // ErrorThis kind of code isn't technically an implicit conversion, but the language treats it as one as far as
explicit
is concerned.
- 隐式转换可以使类型更可用和更具表现力,无需在明显时显式命名类型。
- 隐式转换可以是重载的更简单的替代方法,例如,当具有
string_view
参数的单个函数取代std::string
和const char_
的单独重载时。 - 列表初始化语法是初始化对象的一种简洁且富有表现力的方法。
- 隐式转换可以隐藏类型不匹配的错误,其中目标类型与用户的期望不匹配,或者用户不知道将发生任何转换。
- 隐式转换会使代码更难阅读,尤其是在存在过载的情况下,因为实际调用的代码不太明显。
- 采用单个参数的构造函数可能会意外地用作隐式类型转换,即使它们不打算这样做。
- 当单参数构造函数未标记为
显式
时,没有可靠的方法来判断它是否旨在定义隐式转换,或者作者只是忘记标记它。 - 并不总是清楚哪个类型应该提供转换,如果两者都提供转换,代码就会变得不明确。
- 如果目标类型是隐式的,则列表初始化可能会遇到同样的问题,尤其是在列表只有一个元素时。
类型转换运算符和可用单个参数调用的构造函数必须在类定义中显式
标记。 作为例外,复制和移动构造函数不应是显式
的,因为它们不执行类型转换。 隐式转换有时可能是必要的,并且适用于旨在透明包装其他类型的类型。 在这种情况下,请联系您的项目主管请求放弃此规则。
不能用单个参数调用的构造函数可能会省略显式
。 接受单个 std::initializer_list
参数的构造函数还应省略 explicit
,以支持复制初始化(例如 MyType m = {1, 2};
)。
可拷贝类型和可移动类型
类的公共 API 必须明确该类是可复制的、仅移动的,还是既不可复制又不可移动的。 如果这些操作对您的类型清晰且有意义,则支持复制和/或移动。
可移动类型是可以从临时阶段初始化和分配的类型。
可复制类型是可以从同一类型的任何其他对象初始化或分配的类型(因此,根据定义也是可移动的),并规定源的值不会更改。
std::unique_ptr<int>
是可移动但不可复制的类型的示例(因为源std 的值::unique_ptr<int>
必须在分配给目的地时修改)。 int
和std::string
是可移动类型的示例,也可以复制。 (对于int,
移动和复制操作是相同的;对于std::string,
存在比复制成本低的移动操作。
对于用户定义的类型,复制行为由复制构造函数和复制赋值运算符定义。 移动行为由移动构造函数和移动赋值运算符(如果存在)或复制构造函数和复制赋值运算符定义。
在某些情况下,编译器可以隐式调用复制/移动构造函数,例如,在按值传递对象时。
可复制和可移动类型的对象可以通过值传递和返回,这使得 API 更简单、更安全、更通用。 与通过指针或引用传递对象时不同,不存在在所有权、生存期、可变性和类似问题方面混淆的风险,也无需在协定中指定它们。 它还可防止客户端和实现之间的非本地交互,从而使其更易于编译器理解、维护和优化。 此外,此类对象可用于需要按值传递的通用 API(如大多数容器),并且它们允许在类型组合中实现更大的灵活性。
复制/移动构造函数和赋值运算符通常比Clone()、CopyFrom()
或Swap()
等替代方法更容易正确定义,因为它们可以由编译器生成,或者使用@ 默认值
。 它们简洁,可确保复制所有数据成员。 复制和移动构造函数通常也更有效,因为它们不需要堆分配或单独的初始化和分配步骤,并且它们符合优化条件,如复制删除。
移动操作允许从 rvalue 对象中隐式有效地传输资源。 在某些情况下,这允许更简单的编码样式。
某些类型不需要可复制,为此类类型提供复制操作可能令人困惑、荒谬或完全不正确。 表示单个对象 (注册器
), 绑定到特定作用域 (清理
) 或紧密耦合到对象标识 (Mutex
) 的对象的类型不能有意义地复制。
要多态地使用的基类类型的复制操作是危险的,因为使用它们可能导致对象切片。
默认或不小心实现的副本操作可能不正确,并且生成的 Bug 可能令人困惑且难以诊断。
隐式调用复制构造函数,这使得调用容易错过。 这可能会给习惯于传统或强制引用语言的程序员造成混淆。 它还可能鼓励过度复制,这可能导致性能问题。
每个类的公共接口必须明确类支持哪些复制和移动操作。 这通常应采取显式声明和/或删除声明公共
部分中的适当操作的形式。
具体而言,可复制类应显式声明复制操作,仅移动类应显式声明移动操作,不可复制/可移动类应显式删除复制操作。 允许显式声明或删除所有四个复制/移动操作,但不是必需的。 如果提供副本或移动赋值运算符,则还必须提供相应的构造函数。
class Copyable { public: Copyable(const Copyable& other) = default; Copyable& operator=(const Copyable& other) = default; // The implicit move operations are suppressed by the declarations above. }; class MoveOnly { public: MoveOnly(MoveOnly&& other); MoveOnly& operator=(MoveOnly&& other); // The copy operations are implicitly deleted, but you can // spell that out explicitly if you want: MoveOnly(const MoveOnly&) = delete; MoveOnly& operator=(const MoveOnly&) = delete; }; class NotCopyableOrMovable { public: // Not copyable or movable NotCopyableOrMovable(const NotCopyableOrMovable&) = delete; NotCopyableOrMovable& operator=(const NotCopyableOrMovable&) = delete; // The move operations are implicitly disabled, but you can // spell that out explicitly if you want: NotCopyableOrMovable(NotCopyableOrMovable&&) = delete; NotCopyableOrMovable& operator=(NotCopyableOrMovable&&) = delete; };
这些声明/删除只有在显而易见时才能省略:
- 如果类没有
私有
节(如结构或仅接口基类),则可以由任何公共数据成员的可复制性/可移动性来确定。 - 如果基类显然不可复制或可移动,派生类自然也不会是。 仅接口基类将这些操作保留在隐式中,这不足以使具体的子类变得清晰。
- 请注意,如果显式声明或删除复制的构造函数或赋值操作,则其他复制操作并不明显,必须声明或删除。 同样适用于移动操作。
如果对普通用户来说复制/移动的含义不明确,或者它会产生意外的成本,则类型不应是可复制/可移动的。 可复制类型的移动操作严格而言是性能优化,是 Bug 和复杂性的潜在来源,因此除非它们比相应的复制操作更高效,否则请避免定义它们。 如果类型提供复制操作,建议您设计类,以便这些操作的默认实现正确。 请记住,要像查看任何其他代码一样,查看任何默认操作的正确性。
由于切片的风险,倾向于避免为拟派生的类提供公共赋值运算符或复制/移动构造函数(并且倾向于避免从具有此类成员的类派生)。 如果基类需要可复制,请提供公共虚拟Clone()
方法,以及派生类可用于实现它的受保护复制构造函数。
结构体 vs. 类
仅对传输数据的被动对象使用结构
;其他一切都是一个类
。
结构
关键字和类
关键字在C++中几乎相同。 我们为每个关键字添加自己的语义含义,因此您应该对所定义的数据类型使用适当的关键字。
结构
应用于承载数据的被动对象,并且可能具有关联的常量,但除了访问/设置数据成员之外,它缺乏任何其他功能。 所有字段必须是公共的,并且直接访问,而不是通过 getter/setter 方法访问。 结构不能具有暗示不同字段之间关系的不变量,因为直接用户访问这些字段可能会破坏这些不变性。 方法不应提供行为,而应仅用于设置数据成员,例如,构造函数、析构函数、初始化()
、Reset()。
如果需要更多的功能或固定性,则类
更合适。 如果有疑问,把它做为类
。
为了与 STL 保持一致,您可以将结构
而不是类
用于无状态类型,如特征、模板元函数和一些 functor。
请注意,结构类和类中的成员变量具有不同的命名规则。
结构体 vs. Pairs 和 Tuples
当元素可以具有有意义的名称时,最好使用结构
而不是对或元组。
虽然使用对和元组可以避免定义自定义类型,从而在编写代码时可能节省工作,但有意义的字段名称在读取代码时几乎总是比.first、.
秒
或std::get<X>
。
虽然 C++14 引入std::get_lt;Type>
按类型而不是索引访问元组元素(当类型是唯一的)有时可以部分缓解这种情况,但字段名称通常比类型更清晰且信息更丰富。
在组合或元组的元素没有特定含义的泛型代码中,对和元组可能适用。 为了与现有代码或 API 互操作,可能还需要使用它们。
继承
组合通常比继承更合适。
使用继承时,将其公开
。
当子类从基类继承时,它包括基类定义的所有数据和操作的定义。 "接口继承"是从纯抽象基类(一个没有状态或定义的方法)继承;所有其他继承都是"实现继承"。
实现继承通过重用基类代码来减少代码大小,因为它专门化了现有类型。 由于继承是编译时声明,因此您和编译器可以理解操作并检测错误。 接口继承可用于以编程方式强制类公开特定的 API。 同样,编译器可以检测错误,在这种情况下,当类不定义 API 的必要方法时。
对于实现继承,由于实现子类的代码在基类和子类之间分布,因此理解实现可能更加困难。 子类不能重写非虚拟函数,因此子类不能更改实现。
多重继承尤其成问题,因为它通常会带来更高的性能开销(事实上,从单个继承到多个继承的性能下降通常大于从普通到虚拟的性能下降调度),因为它有可能导致"菱形"继承模式,这种模式容易出现歧义、混乱和直接错误。
所有继承都应是公开的
。 如果要执行私有继承,则应将基类的实例作为成员。
不要过度使用实现继承。 作文通常更合适。 尝试将继承的使用限制在"是"的情况下:Bar
子类Foo,
如果可以合理地说,Bar"
是一种"Foo。
将受保护
函数的使用限制为可能需要从子类访问的成员函数。 请注意,数据成员应该是私有的。
使用完全一个重写
或(不太频繁)最终
指定符对虚拟函数或虚拟析构函数的显式批示。 声明重写时不要使用虚拟
。
阐释:标记为重写
或最终
不是基类虚拟函数重写的函数或析构函数将不会编译,这有助于捕获常见错误。 说明符用作文档;如果没有指定符,读取器必须检查相关类的所有祖先,以确定函数或析构函数是否为虚拟。
允许多次继承,但强烈建议不要执行多个实现继承。
运算符重载
重载运算符明智。 不要使用用户定义的文本。
C++允许用户代码使用运算符
关键字声明内置运算符的重载版本,只要其中一个参数是用户定义的类型。 运算符
关键字还允许用户代码使用运算符"定义
新文本类型",并定义类型转换函数,如运算符 bool()
。
运算符重载可以使用户定义的类型的行为与内置类型相同,从而使代码更加简洁和直观。 重载运算符是某些操作(例如 ==
、<
、=
和 <<
)的惯用名称, 遵守这些约定可以使用户定义的类型更具可读性,并使它们能够与期望这些名称的库进行互操作。
用户定义文本是创建用户定义类型的对象的非常简洁的表示法。
- 提供正确、一致且不令人惊讶的运算符重载集需要一些小心,如果不这样做,可能会导致混乱和错误。
- 过度使用运算符可能会导致代码模糊化,尤其是在重载运算符的语义不遵循约定时。
- 功能过载的危险同样适用于操作员过载,如果不是更多的话。
- 操作员超载会欺骗我们的直觉,以为昂贵的操作是廉价的内置操作。
- 查找重载操作员的呼叫站点可能需要一个了解C++语法的搜索工具,而不是 grep。
- 如果重载运算符的参数类型错误,则可能会得到不同的重载而不是编译器错误。 例如
,foo < bar
可以执行一件事,而& foo < & bar
执行一些完全不同的事情。 - 某些操作员过载本身是危险的。
根据重载声明是否可见,重载一元
和
可能导致同一代码具有不同的含义。&、、
和(
逗号)的重载无法与内置运算符的评估顺序语义匹配。
- 运算符通常在类之外定义,因此存在不同文件引入同一运算符的不同定义的风险。 如果两个定义都链接到同一个二进制文件中,则会导致未定义的行为,这可能表现为微妙的运行时 Bug。
- 用户定义的文本 (ULL) 允许创建新的语法形式,即使对有经验的C++程序员来说也是陌生的,例如
"你好世界"sv
作为std::string_view("Hello World")
的速记。 现有的符号更清楚,但简洁。 - 因为它们不能由命名空间限定,因此使用 ULL 还需要使用 using 指令(我们禁止)或使用声明(除非导入的名称是相关头文件公开的接口的一部分,否则我们禁止在头文件中使用)。 考虑到头文件必须避免 UDL 后缀,我们倾向于避免标题文件和源文件之间文本约定不同。
仅当重载运算符的含义显而易见、不奇怪且与相应的内置运算符一致时,才定义重载运算符。 例如,将*
用作位式或逻辑或,而不是作为外壳样式的管道。
仅定义您自己的类型的运算符。 更确切地说,将它们定义为与它们操作的类型相同的标头、.cc 文件和命名空间。 这样,无论类型在哪里,运算符都可用,从而将多个定义的风险降至最低。 如果可能,请避免将运算符定义为模板,因为它们必须满足此规则,以便获得任何可能的模板参数。 如果定义运算符,请同时定义任何有意义的相关运算符,并确保它们定义一致。 例如,如果重载<
,则重载所有比较运算符,并确保<
和>
永远不会为相同的参数返回 true。
更愿意将非修改二进制运算符定义为非成员函数。 如果二进制运算符定义为类成员,则隐式转换将应用于右侧参数,但不适用于左侧参数。 如果< b
编译,但 b <
a 编译,则会使用户感到困惑。
不要为了避免定义运算符重载而走自己的路。 例如,更倾向定义 ==
、=
和 <<
而不是 Equals()
、CopyFrom()
和 PrintTo()
。 相反,不要仅仅因为其他库需要它们而定义运算符重载。 例如,如果类型没有自然排序,但您希望将其存储在std::set
中,请使用自定义比较器,而不是重载<;
不要重载 &&
、||
、,
(逗号) 或一元 &
。 不要重载 operator""
,即不引入用户定义的文本。 请勿使用他人(包括标准库)提供的任何此类文本。
类型转换运算符在隐式转换部分中介绍。
有关复制构造函数的部分介绍了_
运算符。 重载<<
用于流,在流部分中介绍。 另请参阅函数重载的规则 ,这些规则也适用于运算符重载。
访问控制
使类的数据成员成为私有
,除非它们是常量。 这简化了对不变量的推理,但必要时以访问器(通常是const)
的形式,以一些简单的样板为代价。
出于技术原因,在使用Google Test时,我们允许 .cc 文件中测试固件类的数据成员受到保护
。
声明顺序
将类似的声明组合在一起,更早地放置公共部分。
类定义通常应以
public:
部分开始,后跟
protected:
,然后是 private:
。 如果某个部分没有则省略。
在每个部分中,通常喜欢将相似类型的声明分组在一起,并且通常更喜欢以下顺序:类型(包括typedef、using
和嵌套结构与类)、常量项、工厂函数、构造函数、赋值运算符、析构函数、所有其他方法、数据成员。
不要将大型方法定义内联放在类定义中。 通常,只能内联定义简单或性能关键且非常短的方法。 有关详细信息,请参阅内联函数。
函数
输出参数
C++函数的输出自然通过返回值提供,有时通过输出参数提供。
比起输出参数,更喜欢使用返回值:它们提高了可读性,并且通常提供相同或更好的性能。 如果使用仅输出参数,则应在输入参数后显示这些参数。
参数是函数的输入、函数的输出或两者。 输入参数通常是值或const
引用,而输出和输入/输出参数将指向非const
。
排序函数参数时,将所有仅输入参数放在任何输出参数之前。 特别是,不要仅仅因为它们是新的,就向函数的末尾添加新参数;将新的仅输入参数放在输出参数之前。
这不是一个硬性的规则。 输入和输出(通常是类/结构)的参数使水浑浊,并且与一如既往,与相关函数的一致性可能需要您弯曲规则。
编写简短函数
首选小型且重点突出的功能。
我们认识到,长函数有时是合适的,因此对函数长度没有硬性限制。 如果函数超过大约 40 行,则考虑是否可以在不损害程序结构的情况下将其分解。
即使您的长函数现在工作正常,在几个月内修改它的人可能会添加新的行为。 这可能会导致难以找到的 Bug。 保持函数简短和简单,使其他人更容易阅读和修改您的代码。 小函数也更易于测试。
使用某些代码时,可能会找到长而复杂的函数。 不要被修改现有代码吓倒:如果使用这样的函数证明很困难,您发现错误很难调试,或者您希望在几个不同的上下文中使用它的一部分,请考虑将函数分解为更小和更多可管理件。
引用参数
由 lvalue 引用传递的所有参数都必须标记为const
。
在 C 中,如果函数需要修改变量,则参数必须使用指针,例如 int foo(int *pval)
。 在 C++ 中,函数可以声明一个引用参数:int foo(int &val)
。
将参数定义为引用可避免像(_pval)*
那样的难看代码。 某些应用程序(如复制构造函数)是必需的。 与指针不同,请说明空指针不是可能的值。
引用可能会令人困惑,因为它们具有值语法,但具有指针语义。
在函数参数列表中,所有引用都必须为const
:
void Foo(const std::string &in, std::string *out);
事实上,在 Google 代码中,输入参数是值或const
引用,而输出参数是指针,这是一个非常强的约定。 输入参数可能是const
指针,但我们绝不允许非const
引用参数,除非约定要求,例如,swap()
。
但是,在某些情况下,使用const T+
比const T&
用于输入参数更可取。 例如:
- 您想要传递一个空指针。
- 函数保存对输入的指针或引用。
请记住,大多数时间输入参数将指定为const T&。
使用const T+
代替向读取器传达输入以某种方式受到不同的处理。 因此,如果您选择const T+
而不是const T&,
这样做是出于具体原因;否则,它可能会混淆读者,使他们寻找一个不存在的解释。
函数重载
仅当查看调用站点的读者能够清楚地了解正在发生的情况,而无需首先准确确定调用哪个重载时,才使用重载函数(包括构造函数)。
可以编写一个函数,该函数采用const std::string;
并将其与另一个采用const char_
的函数重载。 但是,在这种情况下,请考虑 std::string_view。
class MyClass { public: void Analyze(const std::string &text); void Analyze(const char *text, size_t textlen); };
重载允许名称相同的函数采用不同的参数,从而使代码更加直观。 对于模板化代码,它可能需要,并且对访问者来说可能很方便。
基于 const 或 ref 限定的重载可能会使实用程序代码更有用、更高效,或者两者兼而有之。 (更多信息请参阅 TotW 148)。
如果函数仅由参数类型重载,则读者可能必须了解C++的复杂匹配规则,以便判断发生了什么。 此外,如果派生类仅重写函数的某些变体,则许多人对继承的语义感到困惑。
当变体之间没有语义差异时,可能会重载函数。 这些重载在类型、限定符或参数计数中可能有所不同。 但是,此类调用的读取器不必知道重载集的哪个成员被选中,而只需要知道正在调用该集中的内容。 如果可以使用标头中的单个注释记录重载集中的所有条目,则这是一个很好的迹象,表明它是设计良好的过载集。
默认参数
当保证默认值始终具有相同的值时,允许在非虚拟函数上使用默认参数。 遵循与函数重载相同的限制,如果默认参数获得的可读性不大于下面的缺点,则首选重载函数。
通常,您有一个使用默认值的函数,但偶尔要覆盖默认值。 默认参数允许一种简单的方法来执行此操作,而无需为罕见的异常定义许多函数。 与重载函数相比,默认参数的语法更简洁,样板更少,对"必需"和"可选"参数的区分更明确。
默认参数是实现重载函数语义的另一种方法,因此应用不重载函数的所有原因。
虚拟函数调用中参数的默认值由目标对象的静态类型确定,并且不能保证给定函数的所有重写都声明相同的默认值。
默认参数在每个调用站点重新评估,这可能会膨胀生成的代码。 读者还可能期望在声明处固定默认值,而不是在每个调用时进行更改。
在存在默认参数时,函数指针会令人困惑,因为函数签名通常与调用签名不匹配。 添加函数重载可以避免这些问题。
虚拟函数上禁止默认参数,这些参数无法正常工作,并且指定的默认值可能无法根据计算时间计算到相同的值。 (例如,不要这样写 void f(int n = counter++);
)。
在其他一些情况下,默认参数可以改进其函数声明的可读性,足以克服上述缺点,因此允许它们。 如有疑问,请使用重载。
Trailing Return Type Syntax
仅当使用普通语法(前导返回类型)不切实际或可读性低时,才使用尾随返回类型。
C++允许两种不同的函数声明形式。 在较旧的窗体中,返回类型显示在函数名称之前。 例如:
int foo(int x);
C++11 中引入的较新的窗体在函数名称之前使用auto
关键字,在参数列表之后使用尾随返回类型。 例如,上面的声明可以等效地编写:
auto foo(int x) -> int;
尾随返回类型位于函数的作用域中。 对于像int
这样的简单案例来说,这没有区别,但对于更复杂的情况(如在类作用域中声明的类型或根据函数参数编写的类型)来说,这并不重要。
尾随返回类型是显式指定lambda 表达式的返回类型的唯一方法。 在某些情况下,编译器能够推断 lambda 的返回类型,但并非在所有情况下都能够推断出 lambda 的返回类型。 即使编译器可以自动推断它,有时显式指定它对于读者来说也会更清楚。
有时,在函数的参数列表出现后指定返回类型更容易、更可读。 当返回类型依赖于模板参数时,尤其如此。 例如:
template <typename T, typename U> auto add(T t, U u) -> decltype(t + u);versus
template <typename T, typename U> decltype(declval<T&>() + declval<U&>()) add(T t, U u);
尾随返回类型语法相对较新,并且没有类似C++语言(如 C 和 Java)的模拟,因此一些读者可能会觉得它不熟悉。
现有的代码库有大量的函数声明,这些声明不会因为使用新语法而发生更改,因此现实的选择是只使用旧语法或同时使用两者。 使用单一版本可以更好地统一样式。
在大多数情况下,继续使用函数声明的旧样式,其中返回类型位于函数名称之前。 仅在需要时(如 lambdas)或通过将类型放在函数的参数列表之后,才使用新的尾随-返回类型窗体,从而允许您以更具可读性的方式编写类型。 后一种情况应该很少;这主要是相当复杂的模板代码中的问题,在大多数情况下,这是不鼓励的。
Google-Specific Magic
我们使用各种技巧和实用程序使C++代码更加健壮,我们使用C++可能与您在其他地方看到的不同。
Ownership and Smart Pointers
最好为动态分配的对象具有单个固定所有者。 更喜欢使用智能指针转移所有权。
"所有权"是一种用于管理动态分配的内存(和其他资源)的簿记技术。 动态分配的对象的所有者是一个对象或函数,负责确保在不再需要时将其删除。 所有权有时可以共享,在这种情况下,最后一个所有者通常负责删除它。 即使所有权不是共享的,也可以将其从一段代码转移到另一段代码。
"智能"指针是类似于指针的类,例如通过重载+
和->
运算符。 某些智能指针类型可用于自动进行所有权簿记,以确保这些职责得到满足。
st::unique_ptr
是 C++11 中引入的智能指针类型,表示动态分配对象的独占所有权;当std::unique_ptr
超出范围时,将删除该对象。
无法复制它,但可以移动以表示所有权转移。
st::shared_ptr
是一种智能指针类型,表示动态分配对象的共享所有权。 std::shared_ptr
可以复制;对象的所有权在所有副本之间共享,并在销毁最后一个std::shared_ptr
时删除该对象。
- 如果没有某种所有权逻辑,几乎不可能管理动态分配的内存。
- 转移对象的所有权可能比复制对象(如果复制甚至有可能)要便宜。
- 转移所有权可能比"借用"指针或引用更简单,因为它减少了协调两个用户之间对象生存期的需要。
- 智能指针可以通过使所有权逻辑明确、自我记录和明确性来提高可读性。
- 智能指针可以消除手动所有权簿记,简化代码并排除大类错误。
- 对于 const 对象,共享所有权可以是深度复制的简单且高效的替代方法。
- 所有权必须通过指针(无论是智能的还是普通的)来表示和转移的。 指针语义比值语义复杂,尤其是在 API 中:您不仅要担心所有权,还要担心别名、生存期和可变性等问题。
- 价值语义的性能成本经常被高估,因此所有权转移的性能优势可能不足以证明可读性和复杂性成本的合理性。
- 转移所有权的 API 强制其客户端进入单个内存管理模型。
- 使用智能指针的代码对资源发布的发生位置不太明确。
std::unique_ptr
使用C++11的移动语义表示所有权转移,这些语义相对较新,可能会混淆一些程序员。- 共享所有权可能是一个诱人的替代,以仔细的所有权设计,混淆系统的设计。
- 共享所有权要求在运行时进行显式簿记,这可能成本高昂。
- 在某些情况下(例如循环引用),具有共享所有权的对象可能永远不会被删除。
- 智能指针不是普通指针的完美替代品。
如果需要动态分配,则最好使用分配它的代码保留所有权。 如果其他代码需要访问该对象,请考虑将其传递为副本,或传递指针或引用而不转移所有权。 首选使用std::unique_ptr
使所有权转移显式化。 例如:
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
分别。
其它 C++ 特性
右值引用
使用 rvalue 引用:
- 定义移动构造函数和移动赋值运算符。
- 如果有证据表明,这比按值传递提供有意义的性能更好,或者如果您正在编写需要支持任意类型的低开销泛型代码,则使用 const+ 和 + 变体定义重载集。 请注意组合重载集,即很少重载多个参数。
- 支持通用代码中的"完美转发"。
Rvalue 引用是一种只能绑定到临时对象的引用类型。 语法类似于传统的引用语法。 例如,void f(std::string&s);
声明一个函数,其参数是对 std::string 的 rvalue 引用。
当标记"*"应用于函数参数中的非限定模板参数时,将应用特殊模板参数推导规则。 此类引用称为转发引用。
- 定义移动构造函数(采用对类类型的 rvalue 引用的构造函数)可以移动值,而不是复制它。 例如
,如果 v1
是std::vector_lt;std::string>,
则自动 v2(std::move(v1))
可能只是导致一些简单的指针操作,而不是复制大量数据。 在许多情况下,这可能导致性能的重大改进。 - Rvalue 引用可以实现可移动但不可复制的类型,这对于没有合理定义的复制类型,但您可能仍希望将它们传递为函数参数,将它们放入容器等类型非常有用。
std::移动
是有效利用一些标准库类型(如std::unique_ptr
)所必需的。- 转发使用 rvalue 引用令牌的引用,可以编写一个泛型函数包装器,将其参数转发到另一个函数,并且无论其参数是否为临时对象和/或 const,都有效。 这称为"完美转发"。
- Rvalue 引用尚未被广泛理解。 引用折叠和转发引用的特殊扣除规则等规则有些模糊。
- Rvalue 引用经常被误用。 在函数调用后,参数预期具有有效指定状态,或者未执行移动操作的签名中,使用 rvalue 引用是违反直觉的。
可以使用 rvalue 引用来定义移动构造函数和移动赋值运算符(如"可复制和可移动类型"中所述)。 有关移动语义和std::move
的详细信息,请参阅C++入门。
您可以使用 rvalue 引用来定义重载对,一个采用Foo&amp;
另一个使用const Foo&
。
通常首选解决方案只是传递值,但重载函数对有时会产生更好的性能,有时在需要支持各种类型的泛型代码中是必需的。 与往常一样:如果您为了性能而编写更复杂的代码,请确保有证据证明它实际上有帮助。
您可以将转发引用与
std::前进
,支持完美的转发。
友元
我们允许在合理范围内使用好友
类和函数。
朋友通常应该在同一文件中定义,以便读者不必查看另一个文件来查找类的私有成员的用途。 朋友
的一个常见用途是让FooBuilder
类成为Foo
的朋友,这样它就可以正确地构造Foo
的内部状态,而不会将这种状态暴露给世界。 在某些情况下,将单元测试类作为它所测试的类的朋友可能很有用。
朋友扩展,但不中断,类的封装边界。 在某些情况下,当您希望仅授予其他一个类对成员的访问权限时,这比公开成员更好。 但是,大多数类应仅通过其公共成员与其他类进行交互。
异常
我们不C++例外。
- 异常允许应用程序的更高级别决定如何处理深度嵌套函数中的"不可能发生的"故障,而无需隐匿和容易出错的错误代码的簿记。
- 大多数其他现代语言都使用异常。 在C++使用它们将使它更符合Python、Java和其他C++所熟悉的。
- 某些第三方C++库使用异常,在内部关闭这些异常会使其更难与这些库集成。
- 异常是构造函数失败的唯一方法。 我们可以使用工厂函数或
Init()
方法模拟此数据,但分别需要堆分配或新的"无效"状态。 - 异常在测试框架中非常方便。
- 将
throw
语句添加到现有函数时,必须检查其所有传递调用方。 要么他们必须至少做出基本的异常安全保证,要么他们绝不能抓住异常,并且对程序因此终止感到满意。 例如,如果f()
调用g()
调用h()
,h
引发f
捕获的异常,则 g
必须小心,否则可能无法正确清理。 - 更一般地说,异常使程序的控制流难以通过查看代码来评估:函数可能会返回到您不希望的位置。 这会导致可维护性和调试困难。 您可以通过一些关于如何使用异常以及在哪里使用异常的规则来最小化此成本,但代价是开发人员需要了解和理解的更多。
- 异常安全需要 RAII 和不同的编码实践。 需要大量的支持机制,使编写正确的异常安全代码变得容易。 此外,为了避免要求读者了解整个调用图,异常安全代码必须隔离写入持久状态的逻辑进入"提交"阶段。 这将同时具有优势和成本(可能情况下,您被迫混淆代码以隔离提交)。 允许例外会迫使我们总是支付这些成本,即使它们不值得。
- 打开异常会将数据添加到生成的每个二进制文件中,从而增加编译时间(可能略微),并可能增加地址空间压力。
- 异常的可用性可能会鼓励开发人员在它们不合适时丢弃它们,或者在不安全的情况下从它们中恢复。 例如,无效的用户输入不应导致引发异常。 我们需要使样式指南甚至更长的时间来记录这些限制!
从表面上看,使用例外的好处大于成本,尤其是在新项目中。 但是,对于现有代码,引入异常对所有从属代码都有影响。 如果异常可以在新项目之外传播,则将新项目集成到现有的无异常代码中也会产生问题。 由于 Google 的大多数现有C++代码不准备处理异常,因此采用生成异常的新代码比较困难。
鉴于 Google 的现有代码并非容例,因此使用异常的成本略高于新项目的成本。 转换过程将缓慢且容易出错。 我们认为,替代异常(如错误代码和断言)的可用替代方法不会带来重大负担。
我们反对使用例外的建议不是基于哲学或道德理由,而是基于实际理由。 因为我们希望在 Google 使用我们的开源项目,如果这些项目使用例外,则很难这样做,因此我们还需要建议不要在 Google 开源项目中提供例外。 如果我们必须从头开始做一遍,事情可能会有所不同。
此禁令也适用于C++11 中添加的异常处理相关功能,例如std::exception_ptr
和std::nested_exception
。
此规则(不用于双关语)是 Windows 代码的例外。
noexcept
如果有用且正确,请指定 noexcept
。
noexcept
指定符用于指定函数是否将引发异常。 如果异常从标记为noexcept
的函数转义,则程序通过std::终止
崩溃。
Noexcept
运算符执行编译时检查,如果声明表达式不引发任何异常,则返回 true。
- 将移动构造函数指定为
noexcept
在某些情况下可提高性能,例如,std:vector_lt;T>::resize()
移动,而不是复制对象,如果 T 的移动构造函数不是例外
。 - 在函数上指定
noexcept
可以在启用异常的环境中触发编译器优化,例如,编译器不必为堆栈展开生成额外的代码,如果它知道由于noexcept
指定符而无法引发异常。
-
在本指南禁用异常的项目中,很难确保
no 例外
指定符正确,也很难定义正确性甚至含义。 - 很难(如果不是不可能的话)撤消
no,
因为它消除了调用方可能依赖的保证,其方式很难检测。
如果函数准确地反映了函数的预期语义,即如果异常以某种方式从函数体中引发,则它代表一个致命错误,则可以使用no except。"
可以假定移动构造函数中没有任何构造
函数具有有意义的性能优势。 如果您认为在一些其他函数上指定no 除了
具有显著的性能优势,请与您的项目主管讨论。
如果完全禁用异常(即大多数 Google C++环境),则首选无条件例外
。
否则,请使用具有简单条件的条件noexcept
指定符,其方式仅在函数可能引发的少数情况下才计算 false。
测试可能包括类型特征检查所涉及的操作是否可能引发(例如,std:is_nothrow_move_constructible
移动构造对象),或者分配是否可以引发(例如,absl::default_allocator_is_nothrow
用于标准默认分配)。 请注意,在许多情况下,异常的唯一可能原因是分配失败(我们认为移动构造函数不应引发,除非由于分配失败),并且有许多应用程序适合将内存耗尽视为致命错误而不是程序应尝试恢复的异常情况。 即使对于其他潜在的故障,您也应该优先考虑接口简单性,而不是支持所有可能的异常引发方案:而不是编写一个复杂的noexcept
子句,该子句取决于哈希函数是否可以引发,例如,只需记录组件不支持哈希函数引发,并无条件地将其设为"无一例外
"。
运行时类型信息(RTTI)
避免使用运行时类型信息 (RTTI)。
RTTI 允许程序员在运行时查询对象的C++类。 这是通过使用类型化或
dynamic_cast
来完成的。
RTTI 的标准替代方案(如下所述)需要修改或重新设计相关类层次结构。 有时,此类修改是不可行的或不可取的,尤其是在广泛使用或成熟的代码中。
RTTI 在某些单元测试中非常有用。 例如,它在工厂类的测试中很有用,测试必须验证新创建的对象是否具有预期的动态类型。 它还可用于管理对象与其模拟之间的关系。
在考虑多个抽象对象时,RTTI 非常有用。 考虑
bool Base::Equal(Base* other) = 0; bool Derived::Equal(Base* other) { Derived* that = dynamic_cast<Derived*>(other); if (that == nullptr) return false; ... }
在运行时查询对象的类型通常意味着设计问题。 在运行时需要知道对象的类型通常表明类层次结构的设计存在缺陷。
不守纪律地使用 RTTI 会使代码难以维护。 它可能导致基于类型的决策树或切换分散在代码中的语句,在进行进一步更改时必须检查所有这些语句。
RTTI 具有合法用途,但容易被滥用,因此在使用时必须小心。 您可以在单元测试中自由使用它,但尽可能在其他代码中避免使用。 特别是,在新代码中使用 RTTI 之前,请三思而行。 如果您发现自己需要编写基于对象类的行为不同的代码,请考虑以下查询类型的替代项之一:
- 虚拟方法是根据特定子类类型执行不同代码路径的首选方法。 这会将工作置于对象本身中。
- 如果工作属于对象外部,而是在某些处理代码中,请考虑双调度解决方案,如访问者设计模式。 这允许对象本身以外的工具使用内置类型系统确定类的类型。
当程序的逻辑保证基类的给定实例实际上是特定派生类的实例时,可以在对象上自由使用dynamic_cast。
通常,在这种情况下,可以使用static_cast
作为替代方法。
基于类型的决策树强烈表明您的代码在错误轨道上。
if (typeid(*data) == typeid(D1)) {
...
} else if (typeid(*data) == typeid(D2)) {
...
} else if (typeid(*data) == typeid(D3)) {
...
当将其他子类添加到类层次结构时,通常会中断此类代码。 此外,当子类的属性发生更改时,很难找到和修改所有受影响的代码段。
请勿手动实现类似 RTTI 的解决方法。 反对 RTTI 的参数同样适用于解决方法,如具有类型标记的类层次结构。 此外,解决方法会掩盖你的真实意图。
Casting
使用C++样式的强制转换,如static_cast<float>(double_value),
或大括号初始化,用于转换算术类型,如int64 y = int64{1} << 42
。 不要使用int y = (int) x
或int y = int(x)
等强制转换格式(但在调用类类型的构造函数时,后者可以)。
C++引入了与 C 不同的强制转换系统,以区分强制转换操作的类型。
C 强制转换的问题是操作的模糊性;有时你正在做转换(例如,(int)3.5),
有时你正在做转换(例如,(int)"你好"
。 大括号初始化和C++强制转换通常有助于避免这种歧义。 此外,C++强制转换在搜索它们时更加明显。
C++样式的强制转换语法冗长而繁琐。
不要使用 C 样式的强制转换。 相反,当需要显式类型转换时,请使用这些C++样式强制转换。
- 使用大括号初始化转换算术类型(例如
int64{x}
)。 这是最安全的方法,因为如果转换可能导致信息丢失,则代码不会编译。 语法也简洁。 - 使用
static_cast
作为等效于执行值转换的 C 样式强制转换,当您需要显式向上投射指针从类到其超类时,或者当您需要显式将指针从超类转换为子类时。 在最后一种情况下,必须确保对象实际上是子类的实例。 - 使用
const_cast
删除const
限定符(请参见const)。 - 使用
reinterpret_cast
对整数和其他指针类型执行指针类型的不安全转换。 仅当您知道自己在做什么并且了解别名问题时,才使用此选项。 此外,请考虑备选的absl::bit_cast
。 - 使用
absl::bit_cast
使用相同大小的不同类型的(双关语)来解释值的原始位,例如将双
精度位解释为int64
。
有关dynamic_cast
使用的指导,请参阅RTTI 部分。
Streams
在适当情况下使用流,并坚持使用"简单"的用法。 重载<<
仅用于表示值的类型进行流式处理,并且只写入用户可见值,不写入任何实现详细信息。
流是C++的标准 I/O 抽象,标准标头<iostream >
就是例证。
它们广泛用于 Google 代码,主要用于调试日志记录和测试诊断。
<<
> 流
运算符为格式化的 I/O 提供 API,易于学习、可移植、可重用和可扩展。
相比之下,printf
甚至不支持std::string,
更不用说用户定义的类型了,而且很难用到。
printf
还允许您从该函数的众多略有不同的版本中进行选择,并导航数十个转换规范器。
流通过 std::cin、std::cout、st::cer 和std::clog
为控制台 I/O 提供一流的支持。
C API 也一样,但因需要手动缓冲输入而受阻。
- 可以通过更改流的状态来配置流格式。 此类突变是永久性的,因此代码的行为可能会受流的整个以前历史记录的影响,除非您在每次其他代码可能触及流时都竭尽全力将其还原到已知状态。 用户代码不仅可以修改内置状态,还可以通过注册系统添加新的状态变量和行为。
- 由于上述问题、代码和数据在流代码中混合的方式以及运算符重载的使用(可能会选择与预期不同的重载),因此很难精确控制流输出。
- 通过
<<
运算符链建立输出的做法干扰了国际化,因为它将字顺序烘焙到代码中,并且流对本地化的支持是有缺陷的。 - 流 API 是微妙而复杂的,因此程序员必须开发具有它的经验才能有效地使用它。
- 对于编译器来说,解决
<<.
的许多重载是非常昂贵的。 当在大型代码库中广泛使用时,它可以使用多达 20% 的解析和语义分析时间。
仅当流是作业的最佳工具时才使用流。
当 I/O 是临时的、本地的、可人读的,并且针对其他开发人员而不是最终用户时,通常会出现这种情况。 与周围的代码以及整个代码库保持一致;如果有针对您的问题的既定工具,请使用该工具。
特别是,日志记录库通常比std::cerr
或std::clog
用于诊断输出更好,并且absl/字符串
或等效项中的库通常比std::stringstream
更好选择。
避免将流用于面向外部用户或处理不受信任的数据的 I/O。 相反,找到并使用适当的模板库来处理国际化、本地化和安全强化等问题。
如果使用流,请避免流 API 的有状态部分(错误状态之外),如imbue()、xalloc()
和register_callback()。
使用显式格式设置函数(请参阅
absl/字符串
),而不是流操纵器或格式标记来控制格式设置详细信息,如数字基础、精度或填充。
重载<<
作为类型的流运算符,仅当您的类型表示值时,<<
写出该值的人为可读的字符串表示形式。 避免在<< 和 .;
如果需要打印对象内部进行调试,请使用命名函数(名为DebugString()
的方法是最常见的约定)。
Preincrement and Predecrement
使用迭代器和其他模板对象的增量和递减运算符的前缀形式 (_i
)。
当变量递增([i
或i]
) 或递减(-i
或i-
) 且表达式的值未使用时,必须决定是预增量(递减)还是后增量(递减)。
当忽略返回值时,"前"窗体 (_i
) 的效率永远不会低于"post" 窗体 (i#
),并且通常效率更高。 这是因为后增量(或递减)需要创建i
的副本,这是表达式的值。 如果我是
迭代器或其他非标量类型,复制我
可能很贵。 由于忽略值时,两种类型的增量功能相同,为什么不总是预增量?
在 C 中发展起来的传统是在不使用表达式值时使用后增量,尤其是在循环中
。 有些人发现后增量更容易阅读,因为"主题" (i
) 先于"动词" (*
), 就像在英语中一样.
对于简单的标量(非对象)值,没有理由选择一种形式,我们允许。 对于迭代器和其他模板类型,请使用预增量。
const 的使用
在 API 中,只要有意义,就使用const
。
对于某些使用 const 的场景,constexpr
是一个更好的选择。
声明的变量和参数前面可以有关键字const
来指示变量未更改(例如,const int foo
)。 类函数可以具有const
限定符来指示函数不更改类成员变量的状态(例如,类 Foo = int Bar(char c) const;;;
。
更容易让人们理解变量的使用方式。 允许编译器执行更好的类型检查,并且可以想象生成更好的代码。 帮助人们说服自己程序的正确性,因为他们知道他们调用的功能是有限的,他们可以修改你的变量。 帮助人们了解在多线程程序中无需锁即可安全使用哪些功能。
const
是病毒性的:如果将const
变量传递给函数,该函数的原型中必须具有const(
否则该变量将需要const_cast)。
在调用库函数时,这可能是一个特定问题。
我们强烈建议在 API 中使用const(
即函数参数、方法和非局部变量),无论它有意义且准确。 这提供了一致的、主要是编译器验证的文档,这些文档包含操作可以变异的对象。 拥有一致且可靠的方法来区分读取和写入对于编写线程安全代码至关重要,并且在许多其他上下文中也很有用。 特别是:
- 如果函数保证不会修改通过引用或指针传递的参数,则相应的函数参数应分别为对 const 的引用
(const T&
) 或指针到 const(const T+
)。 - 对于由值传递的函数参数
,const
对调用方没有影响,因此在函数声明中不建议这样做。 请参阅托特瓦#109。 - 声明方法为
const,
除非它们更改对象的逻辑状态(或允许用户修改该状态,例如通过返回非 const 引用,但很少见),否则它们不能同时安全地调用它们。
既不鼓励也不劝阻对局部变量使用const。
类的所有const
操作应可以安全地同时调用。 如果这不可行,则必须将类明确记录为"线程不安全"。
在哪里放置 const
有些人喜欢这种形式
。
他们认为,这是更具可读性,因为它更一致:它保持的规则,康斯特
总是遵循它描述的对象。 但是,此一致性参数不适用于具有很少深度嵌套指针表达式的代码库,因为大多数const
表达式只有一个const,
并且它适用于基础值。 在这种情况下,没有一致性可维护。 将"组织
"放在第一位可以说是更具可读性,因为它遵循英语将"形容词"(词
名)置于"名词"(int
)之前。
话虽如此,虽然我们鼓励把问题放在第一
位,但我们并不要求它。 但与周围的代码保持一致!
constexpr 的使用
使用constexpr
定义真正的常量或确保恒定初始化。
某些变量可以声明constexpr
以指示变量是真实常量,即固定在编译/链接时间。 某些函数和构造函数可以声明constexpr,
这使他们能够用于定义constexpr
变量。
使用constexpr
可以定义具有浮点表达式的常量,而不仅仅是文本;用户定义类型的常量的定义;和函数调用的常量的定义。
过早地将某些内容标记为 constexpr 可能会导致迁移问题,如果以后必须降级。 当前对 constexpr 函数和构造函数中允许的内容的限制可能会在这些定义中邀请模糊的解决方法。
constexpr
定义支持接口的常量部分的更可靠的规范。 使用constexpr
指定真正的常量和支持其定义的函数。 避免复杂的函数定义,使其与constexpr
一起使用。 请勿使用constexpr
强制内联。
整数类型
在内置C++整数类型中,唯一使用的是int
。 如果程序需要不同大小的变量,请使用<stdint.h>
的精确宽度整数类型,例如int16_t
。 如果变量表示的值可能大于或等于 2^31 (2GiB),请使用 64 位类型,如int64_t
。
请记住,即使您的值对于int
来说不会太大,它也可能用于中间计算,这可能需要更大的类型。 如有疑问,请选择较大的类型。
C++不指定整数类型(如int
)的大小。 通常人们假设短
是 16 位,int
是 32 位,长
是 32 位,长
是 64 位。
声明的统一性。
C++中积分类型的大小可能因编译器和体系结构而异。
<cstdint>
定义int16_t、uint32_t、int64_t
等类型。
当你需要保证整数的大小
时,你应该
总是使用那些短的,没有符号的长长
等。 在 C 整数类型中,只应使用int。
在适当情况下,欢迎您使用标准类型,如size_t
和ptrdiff_t。
我们经常使用int,
因为我们知道的整数不会太大,例如循环计数器。
对这类事情使用普通的旧图
。 您应该假定int
至少为 32 位,但不要假定它有超过 32 位。 如果需要 64 位整数类型,请使用int64_t
或uint64_t
。
对于整数,我们知道可以"大",使用int64_t。
除非存在有效原因(如表示位模式而不是数字)或需要定义溢出 modulo 2_N,否则不应使用无符号整数类型(如uint32_t
。 特别是,不要使用无符号类型来表示数字永远不会是负数。 相反,为此使用断言。
如果代码是返回大小的容器,请确保使用能够容纳容器任何可能用法的类型。 如有疑问,请使用较大的类型而不是较小的类型。
转换整数类型时要小心。 整数转换和促销可能会导致未定义的行为,从而导致安全错误和其他问题。
关于无符号整数
无符号整数非常适合表示位场和模块化算术。 由于历史事故,C++标准也使用无符号整数来表示容器的大小 - 标准机构的许多成员认为这是一个错误,但此时实际上不可能修复。 无符号算术不建模简单整数的行为,而是由模拟模块化算术的标准定义(在溢出/溢出时环绕),这意味着编译器无法诊断出大量 Bug。 在其他情况下,定义的行为会妨碍优化。
也就是说,整数类型的符号混合导致同样大的问题类别。 我们可以提供的最佳建议:尝试使用迭代器和容器,而不是指针和大小,尽量不要混合符号,并尽量避免无符号类型(表示位场或模块化算术除外)。 不要仅仅使用无符号类型来断言变量是非负的。
64 位的可移植性
代码应为 64 位和 32 位友好。 请记住打印、比较和结构对齐的问题。
-
对于某些积分类型,正确的便携式
printf()
转换规范器依赖于我们认为使用令人不快且不切实际的宏扩展(来自<cinttype>
) 的PRI
宏)。 除非没有针对您的特定情况的合理替代方案,否则请尽量避免甚至升级依赖于printf
系列的 API。 而是使用支持类型安全数字格式的库,如StrCat
或Substitute
快速简单转换"或std::ostream
。不幸的是
,PRI
宏是指定标准位宽度类型defs转换的唯一可移植方法(例如int64_t、uint64_t、int32_t、uint32_t
等)。
如果可能,请避免将由位宽度类型def指定的类型的参数传递给基于
printf
的 API。 请注意,可以使用 printf 具有专用长度修饰符的类型,例如size_t
(z)、ptrdiff_t
(t
) 和
maxint_t
(j
)。 - 记住那个
大小(void =)!=
大小(int)。
如果需要指针大小的整数,请使用intptr_t。
- 您可能需要小心结构对齐,尤其是对于存储在磁盘上的结构。 默认情况下,任何
具有int64_t/uint64_t
成员的类/结构在 64 位系统上的 8 字节对齐。 如果在 32 位和 64 位代码之间在磁盘上共享此类结构,则需要确保它们在两个体系结构上都打包相同。 大多数编译器提供了一种更改结构对齐的方法。 对于 gcc,您可以使用
__attribute__(已包装))。
MSVC 提供#pragma包()
和__declspec(对齐())。
-
根据需要使用大括号初始化创建 64 位常量。 例如:
int64_t my_value{0x123456789}; uint64_t my_mask{3ULL << 48};
预处理宏
避免定义宏,尤其是在标头中;首选内联函数、枚举和const
变量。
使用特定于项目的前缀命名宏。 请勿使用宏来定义C++ API 的片段。
宏表示您看到的代码与编译器看到的代码不同。 这会带来意外行为,特别是因为宏具有全局作用域。
宏引入的问题在用于定义C++ API 部分时尤为严重,对于公共 API 则更为严重。 当开发人员错误地使用该接口时,编译器发出的每个错误消息现在都必须解释宏是如何形成接口的。 重构和分析工具在更新界面时要困难得多。 因此,我们明确禁止以这种方式使用宏。 例如,避免使用模式,例如:
class WOMBAT_TYPE(Foo) { // ... public: EXPAND_PUBLIC_WOMBAT_API(Foo) EXPAND_WOMBAT_COMPARISONS(Foo, ==, <) };
幸运的是,宏在C++中不如 C 中是必需的。使用内联函数,而不是使用宏来内联性能关键代码。
使用const
变量,而不是使用宏来存储常量。 使用引用,而不是使用宏来"缩写"长变量名称。
而不是使用宏有条件地编译代码...好吧,不要这样做(当然,除了#define
保护,以防止双重包含头文件)。 这使得测试更加困难。
宏可以执行这些其他技术无法执行的操作,并且您确实在代码库中看到它们,尤其是在较低级别的库中。 它们的一些特殊功能(如字符串化、串联等)无法通过语言本身获得。 但在使用宏之前,请仔细考虑是否有非宏方法实现相同的结果。 如果您需要使用宏来定义接口,请联系您的项目主管请求放弃此规则。
以下使用模式将避免宏的许多问题;如果使用宏,请尽可能遵循它:
- 不要在
.h
文件中定义宏。 - 在使用宏之前
,#define
宏,然后#undef。
- 在用您自己的宏替换现有宏之前,不要只
#undef
它;相反,选择一个可能是唯一的名称。 - 尽量不要使用展开为不平衡的宏C++构造,或者至少很好地记录该行为。
- 最好不使用
*
来生成函数/类/变量名称。
强烈建议不要从标头导出宏(即在标头中定义宏,而不在标头末尾之前#undef
宏)。 如果确实从标头导出宏,则该宏必须具有全局唯一名称。 为此,必须用由项目的命名空间名称(但大写)组成的前缀命名它。
0 和 nullptr/NULL
对指针使用nullptr,
对字符使用"|0"(
而不是0
文本)。
对于指针(地址值),请使用nullptr
,因为这提供了类型安全性。
对于C++03项目,选择NULL
而不是0
。 虽然这些值是等效的,但 NULL
看起来更像指向读取器的指针,而一些C++编译器提供了NULL
的特殊定义,从而能够发出有用的警告。 切勿对数值(整数或浮点)值使用NULL。
对空字符使用"|0"。
使用正确的类型可使代码更具可读性。
sizeof
倾向使用 sizeof(varname)
而不是
sizeof(type)
。
当取特定变量的大小时,请使用sizeof (varname)。
如果某人现在或以后更改变量类型,sizeof(varname)
将相应地更新。 对于与任何特定变量无关的代码,可以使用sizeof(类型),
例如管理外部或内部数据格式的代码,其中适当的C++类型的变量不方便。
struct data; memset(&data, 0, sizeof(data));
memset(&data, 0, sizeof(Struct));
if (raw_size < sizeof(int)) { LOG(ERROR) << "compressed record not big enough for count: " << raw_size; return false; }
类型推导
仅当它使不熟悉项目的读者的代码更清晰,或者它使代码更安全时,才使用类型推导。 不要仅仅为了避免编写显式类型的不便而使用它。
在以下几种上下文中,C++允许(甚至要求)编译器推导出类型,而不是在代码中显式阐明:
- 函数模板参数推导
- 无需显式模板参数即可调用函数模板。
The compiler deduces those arguments from the types of the function
arguments:
template <typename T> void f(T t); f(0); // Invokes f<int>(0)
自动
变量声明- 变量声明可以使用
auto
关键字代替类型。 编译器从变量的初始化器中推断类型,遵循与具有相同初始化器的函数模板参数推导相同的规则(只要不使用大括号而不是括号)。auto a = 42; // a is an int auto& b = a; // b is an int& auto c = b; // c is an int auto d{42}; // d is an int, not a std::initializer_list<int>
auto
can be qualified withconst
, and can be used as part of a pointer or reference type, but it can't be used as a template argument. 此语法的罕见变体使用偏化(自动)
而不是自动
,在这种情况下,推导类型是将decltype
应用于初始化程序的结果。 - 函数返回类型推减
自动
( 和decltype (自动)
也可用于代替函数返回类型. The compiler deduces the return type from thereturn
statements in the function body, following the same rules as for variable declarations:auto f() { return 0; } // The return type of f is int
Lambda expression return types can be deduced in the same way, but this is triggered by omitting the return type, rather than by an explicitauto
. 令人困惑的是,函数的尾随返回类型语法在返回类型位置也使用自动
,但不依赖于类型推导;它只是显式返回类型的备用语法。- 通用羔羊
- lambda 表达式可以使用
auto
关键字代替其一个或多个参数类型。 This causes the lambda's call operator to be a function template instead of an ordinary function, with a separate template parameter for eachauto
function parameter:// Sort `vec` in increasing order std::sort(vec.begin(), vec.end(), [](auto lhs, auto rhs) { return lhs > rhs; });
- 兰姆达init捕获
- Lambda captures can have explicit initializers, which can be used to
declare wholly new variables rather than only capturing existing ones:
[x = 42, y = "foo"] { ... } // x is an int, and y is a const char*
This syntax doesn't allow the type to be specified; instead, it's deduced using the rules forauto
variables. - 类模板参数推导
- 请参阅下文。
- 结构化绑定
- 使用
auto
声明元组、结构或数组时,可以为单个元素指定名称,而不是整个对象的名称;这些名称称为"结构化绑定",整个声明称为"结构化绑定声明"。 This syntax provides no way of specifying the type of either the enclosing object or the individual names:auto [iter, success] = my_map.insert({key, value}); if (!success) { iter->second = value; }
Theauto
can also be qualified withconst
,&
, and&&
, but note that these qualifiers technically apply to the anonymous tuple/struct/array, rather than the individual bindings. 确定绑定类型的规则相当复杂;结果往往并不令人意外,只不过绑定类型通常不会作为引用,即使声明声明引用(但它们通常与引用一样)。 - C++类型名称可能很长且繁琐,尤其是当它们涉及模板或命名空间时。
- 当C++类型名称在单个声明或小代码区域中重复时,重复可能不有助于可读性。
- 有时,推导出类型更安全,因为这可以避免意外复制或类型转换的可能性。
- Lambdas 比其他定义要传递给 STL 算法的函数对象的其他方法要简洁得多,这可以是一种可读性改进。
- 适当使用默认捕获可以删除冗余,并从默认值突出显示重要异常。
- Lambdas、
std::函数
和std::bind
可以组合使用作为通用回调机制;它们使编写将绑定函数作为参数的函数变得容易。 - lambda 中的变量捕获可能是悬空指针 Bug 的来源,尤其是在 lambda 转义当前作用域时。
- 按值进行的默认捕获可能具有误导性,因为它们不能防止悬空指针错误。 按值捕获指针不会导致深度复制,因此它通常具有与引用捕获相同的生存期问题。 在按值捕获"这"时,这尤其令人困惑,因为使用"这"通常是隐式的。
- 捕获实际上声明了新的变量(无论捕获是否具有初始化程序),但它们看起来与C++的任何其他变量声明语法没有什么不同。 特别是,变量的类型,甚至
自动
占位符没有位置(虽然 init 捕获可以间接指示它,例如,使用强制转换)。 这甚至会使它们难以识别为声明。 - Init捕获本质上依赖于类型推导,并且遭受许多与
自动
相同的缺点,还有一个问题,即语法甚至无法提示读者正在进行演绎。 - 使用羔羊可以失控;很长的嵌套匿名函数会使代码更难理解。
- 在适当的情况下使用 lambda 表达式,格式如下所述。
- 如果 lambda 可能转义当前作用域,则首选显式捕获。
例如,而不是:
{ Foo foo; ... executor->Schedule([&] { Frobnicate(foo); }) ... } // BAD! The fact that the lambda makes use of a reference to `foo` and // possibly `this` (if `Frobnicate` is a member function) may not be // apparent on a cursory inspection. If the lambda is invoked after // the function returns, that would be bad, because both `foo` // and the enclosing object could have been destroyed.
喜欢写:{ Foo foo; ... executor->Schedule([&foo] { Frobnicate(foo); }) ... } // BETTER - The compile will fail if `Frobnicate` is a member // function, and it's clearer that `foo` is dangerously captured by // reference.
- 仅当 lambda 的生存期明显短于任何潜在捕获时,才按引用 (*) 使用默认捕获。
- 仅将默认捕获按值 (*) 用作为短 lambda 绑定几个变量的方法,其中捕获的变量集一目了然。 不要用默认按值捕获来写入长或复杂 lambd。
- 仅使用捕获来实际捕获封闭作用域中的变量。 请勿将捕获与初始化程序一起引入新名称,或实质性地更改现有名称的含义。 相反,以常规方式声明一个新变量,然后捕获它,或者避免 lambda 速记并显式定义函数对象。
- 有关指定参数和返回类型的指导,请参阅类型推导部分。
- 从
提升/call_traits调用
特征。 - 从
升压/compressed_pair压缩
对 - 升压图库 (BGL)从
升压/图形
,但序列化(adj_list_serialize.hpp)
和并行/分布式算法和数据结构 (升压/图形/并行/*
和提升/图形/分布式/*
)。 - 属性映射从
提升/property_map,
但并行/分布式属性映射除外 (提升/property_map/并行/*
)。 来自增压/迭代器
的迭代器- 多边形中处理 Voronoi 图构造且不依赖于多边形的其余部分的部分:
提升/多边形/voronoi_builder.hpp、
提升/多边形/voronoi_diagram.hpp
和提升/多边形/voronoi_geometry_type.hpp
- 来自
提升/双映射
的双映射 - 来自
提升/数学/分布
的统计分布和函数 从升压/数学/special_functions
的特殊功能- 来自
提升/数学/工具
的根查找函数 - 来自
提升/multi_index
的多索引 - 提升
/堆
的堆 - 集装箱的扁平集装箱:
升压/集装箱/flat_map,
以及升压/集装箱/flat_set
- 从
提升/侵入
的侵入。 提升/排序
库。- 预处理器从
升压/预处理器
。 - 编译时合理数
(<ratio>
),因为担心它与模板更重的界面样式相关联。 <cfenv>
和<fenv.h>
标头,因为许多编译器不支持这些功能。<system>
标头,它没有足够的测试支持,并且存在固有的安全漏洞。- 非标准扩展可能提供标准C++中不存在的有用功能。 例如,有些人认为指定的初始化器比标准C++函数(如构造函数)更具可读性。
- 只能使用扩展指定编译器的重要性能指南。
- 非标准扩展不适用于所有编译器。 使用非标准扩展会降低代码的可移植性。
- 即使所有目标编译器都支持这些扩展,这些扩展通常也未得到很好的指定,并且编译器之间可能存在细微的行为差异。
- 非标准扩展添加到读者必须了解的语言功能中才能理解代码。
- 别名可以通过简化长名称或复杂名称来提高可读性。
- 别名可以通过在一个位置命名 API 中重复使用的类型来减少重复,这可能会使以后更改类型更加容易。
- 当放置在客户端代码可以引用它们的标头中时,别名会增加该标头 API 中的实体数,从而增加其复杂性。
- 客户端很容易依赖于公共别名的意外详细信息,这使得更改变得困难。
- 创建仅用于实现的公共别名可能很诱人,而不考虑其对 API 或可维护性的影响。
- 别名可能会造成名称冲突的风险
- 别名可以通过为熟悉的构造指定一个不熟悉的名称来降低可读性
- 类型别名可以创建一个不明确的 API 协定:不清楚别名是保证与其别名的类型相同,是具有相同的 API,还是只能以指定的窄方式使用
my_useful_class.cc
my-useful-class.cc
myusefulclass.cc
myusefulclass_test.cc // _unittest和_regtest被弃用。
bigopen()
- 函数名称,以下为
open()
uint
typedef
bigpos
结构
或类
,遵循pos
的形式sparse_hash_map
- 类似 STL 的实体;遵循 STL 命名约定
LONGLONG_MAX
- 常数,如
INT_MAX
- 输入和输出是什么。
- 对于类成员函数:对象是否记住超出方法调用持续时间的引用参数,以及该对象是否会释放它们。
- 如果函数分配调用方必须释放的内存。
- 是否有任何参数可以是空指针。
- 如果函数的使用方式对性能有任何影响。
- 如果该函数是重新进入的。 其同步假设是什么?
- 如果参数是文本常量,并且同一常量在多个函数调用中使用,其方式默认为相同,则应使用命名常量来显式该约束,并保证它保留。
- 请考虑更改函数签名,以将
bool
参数替换为枚举
参数。 这将使参数值自我描述。 - 对于具有多个配置选项的函数,请考虑定义单个类或结构以容纳所有选项,并传递该选项的实例。 这种方法有几个优点。 选项在调用站点按名称引用,从而阐明其含义。 它还减少了函数参数计数,这使得函数调用更易于读写。 作为附加好处,在添加其他选项时,您不必更改呼叫站点。
- 将大型或复杂的嵌套表达式替换为命名变量。
- 作为最后的手段,使用注释来澄清调用站点中的论点含义。
- 不损害可读性、易于剪切和粘贴或自动链接的注释行(例如,如果行包含示例命令或文本 URL 长于 80 个字符)。
- 内容超过 80 个字符的原始字符串文本。 除了测试代码之外,此类文本应显示在文件顶部附近。
- 包含语句。
- 头护罩
- 使用声明
- 选择好的参数名称。
- 仅当函数的定义中未使用参数时,才能省略参数名称。
- 如果无法将返回类型和函数名称放在一行上,则在它们之间换行。
- 如果在函数声明或定义的返回类型之后中断,请不要缩进。
- 打开的括号始终与函数名称位于同一行上。
- 函数名称和打开括号之间从来就没有空格。
- 括号和参数之间永远不会有空格。
- 打开的大括号始终位于函数声明的最后一行的末尾,而不是下一行的开头。
- 右大括号位于最后一行或与打开的大括号相同的行上。
- 在紧密括号和开放式大括号之间应该有一个空格。
- 如果可能,所有参数都应对齐。
- 默认缩进为 2 个空格。
- 包装参数具有 4 个空格缩进。
- 访问成员时,句点或箭头周围没有空格。
- 指针运算符在
*
或&
之后没有空间。 - 任何基类名称都应与子类名称位于同一行,但受 80 列限制的限制。
- 对于
public:
,protected:
和private:
关键字应缩进一个空格。 - 除第一个实例外,这些关键字前面应加上一行空白行。 此规则在小类中是可选的。
- 不要在这些关键字后留空行。
public
部分应在最前面,然后是protected
,最后是private
部分。- 有关以下各节中有关排序声明的规则,请参阅声明顺序。
- 函数开头或结尾处的空白行无助于可读性。
- if-else 块链中的空白行很可能有助于可读性。
- 注释行前面的空白行通常有助于可读性 - 引入新注释表示新思想的开始,而空白行表明注释与以下内容而不是前面内容一致。
- 不要使用匈牙利符号(例如,命名整数
iNum
)。 使用 Google 命名约定,包括源文件的.cc
扩展名。 - Windows为基元类型定义了许多自己的同义词,如
DWORD、HANDLE
等。在调用 Windows API 函数时使用这些类型是完全可以接受的,并且受到鼓励。 即便如此,请尽可能靠近基础C++类型。 例如,使用
const TCHAR =
而不是LPCTSTR
。 - 使用 Microsoft Visual C++编译时,将编译器设置为警告级别 3 或更高,并将所有警告视为错误。
- 不要使用
#pragma一次
;而是使用标准的谷歌包括警卫。 包含防护中的路径应相对于项目树的顶部。 - 事实上,不要使用任何非标准扩展,如
#pragma
和__declspec,
除非你绝对必须。 允许使用__declspec(dllimport)
和__declspec(dllexport);
但是,您必须通过宏(如DLLIMPORT
和DLLEXPORT)
使用它们,以便有人在共享代码时可以轻松地禁用扩展。 - 通常,我们强烈建议不要使用多个实现继承;但是,在使用 COM 和某些 ATL/WTL 类时是必需的。 可以使用多个实现继承来实现 COM 或 ATL/WTL 类和接口。
- 尽管不应在您自己的代码中使用异常,但它们在 ATL 和某些 STL 中广泛使用,包括 Visual C++附带的异常。
使用 ATL 时,应定义
_ATL_NO_EXCEPTIONS
以禁用异常。 您应该调查是否可以在 STL 中禁用异常,但如果禁用,则可以在编译器中打开异常。 (请注意,这只是为了使 STL 编译。 您仍不应自行编写异常处理代码。 - 使用预编译标头的常用方法是在每个源文件的顶部包含一个头文件,通常使用名称如
StdAfx.h
或预编译.
为了使代码更易于与其他项目共享,请避免显式包含此文件(precompile.cc
中除外),并使用/FI
编译器选项自动包含该文件。 - 资源标头通常名为
resource.h,
仅包含宏,不需要符合这些样式准则。
(这些摘要省略了许多细节和注意事项;有关详细信息,请参阅链接。
C++当类型是显式的时,通常代码会更清楚,特别是当类型推导依赖于代码的遥远部分的信息时。 在表达式中,如下所示:
auto foo = x.add_foo(); auto i = y.Find(key);
如果y
的类型不太为人所知,或者y
之前声明了许多行,则结果类型可能并不明显。
程序员必须了解类型推导何时会或不会生成引用类型,或者当他们不是故意获取副本时。
如果将推导的类型用作接口的一部分,则程序员可能会更改其类型,而只打算更改其值,从而导致比预期更激进的 API 更改。
基本规则是:使用类型推导只是为了使代码更清晰或更安全,并且不要仅仅为了避免编写显式类型的不便。 在判断代码是否更清晰时,请记住,您的读者不一定在您的团队中,或熟悉您的项目,因此,您和您的审阅者体验为不必要的杂乱类型通常会为其他人提供有用的信息。 例如,您可以假定make_unique<Foo>()
的返回类型是显而易见的,但MyWidgetFactory()
的返回类型可能不是。
这些原则适用于所有形式的类型扣除,但细节各不相同,如以下各节所述。
函数模板参数推导
函数模板参数推导几乎总是可以的。 类型推导是与函数模板交互的预期默认方式,因为它允许函数模板像无限集的普通函数重载一样运行。 因此,函数模板几乎总是经过设计,以便模板参数推导清晰且安全,或者不编译。
局部变量类型推导
对于局部变量,可以使用类型推导通过消除明显或不相关的类型信息来使代码更清晰,以便读者可以专注于代码的有意义的部分:
std::unique_ptr<WidgetWithBellsAndWhistles> widget_ptr = absl::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2); absl::flat_hash_map<std::string, std::unique_ptr<WidgetWithBellsAndWhistles>>::const_iterator it = my_map_.find(key); std::array<int, 0> numbers = {4, 8, 15, 16, 23, 42};
auto widget_ptr = absl::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2); auto it = my_map_.find(key); std::array numbers = {4, 8, 15, 16, 23, 42};
类型有时包含有用的信息和样板的混合,如上面
的示例中:很明显,该类型是迭代器,并且在许多上下文中容器类型甚至键类型不相关,但值的类型可能很有用。
在这种情况下,通常可以使用传达相关信息的显式类型定义局部变量:
auto it = my_map_.find(key); if (it != my_map_.end()) { WidgetWithBellsAndWhistles& widget = *it->second; // Do stuff with `widget` }If the type is a template instance, and the parameters are boilerplate but the template itself is informative, you can use class template argument deduction to suppress the boilerplate. 然而,这种情况实际上提供了有意义的好处,这种情况很少见。 请注意,类模板参数推导也受单独的样式规则的约束。
如果更简单的选项可以正常工作,请不要使用decltype(自动),
因为它是一个相当模糊的功能,因此在代码清晰度方面成本很高。
返回类型推导
仅当函数体具有非常少量的返回
语句,而其他代码很少时,才使用返回类型推论(对于函数和 lambdas),否则读者可能无法一目了然地判断返回类型是什么。 此外,仅当函数或 lambda 的范围非常窄时才使用它,因为具有推导返回类型的函数不定义抽象边界:实现是接口。 特别是,头文件中的公共函数几乎不应该推导出返回类型。
参数类型推导
应谨慎使用 lambda的自动
参数类型,因为实际类型由调用 lambda 的代码确定,而不是由 lambda 的定义确定。 因此,除非 lambda 被显式调用非常接近定义位置(以便读者可以轻松地同时看到两者),或者 lambda 被传递到一个众所周知的接口,以便它最终调用哪些参数(例如,上面的std::sort
示例),否则显式类型几乎总是会更清楚。
兰姆达init捕获
Init捕获由更具体的样式规则覆盖,它在很大程度上取代了类型推导的一般规则。
结构化绑定
与其他类型的演绎不同,结构化绑定实际上可以通过为较大对象的元素提供有意义的名称来为读者提供附加信息。 这意味着结构化绑定声明可以比显式类型提供净可读性改进,即使在auto
不会的情况下也是如此。 当对象是对或元组(如上面的插入
示例中),结构化绑定特别有益,因为它们没有有意义的字段名称开头,但请注意,除非预先存在的 API(如插入
)强制您使用对或元组,否则通常不应使用对或元组。
如果绑定的对象是一个结构,有时提供更特定于您的用法的名称可能会有所帮助,但请记住,这还可能意味着读者对名称的识别程度低于字段名称。 我们建议使用注释来指示基础字段的名称(如果与绑定的名称不匹配)使用与函数参数注释相同的语法:
auto [/*field_name1=*/ bound_name1, /*field_name2=*/ bound_name2] = ...与函数参数注释一样,这可以使工具能够检测字段的顺序是否错误。
类模板参数推导
仅对已明确选择支持它的模板参数推导使用类模板参数推导。
类模板参数推导(通常缩写为"CTAD")时,使用命名模板的类型声明变量,并且未提供模板参数列表(甚至未提供空尖括号):
std::array a = {1, 2, 3}; // `a` is a std::array<int, 3>The compiler deduces the arguments from the initializer using the template's "deduction guides", which can be explicit or implicit.
显式推导参考线类似于带有尾随返回类型的函数声明,只不过没有前导自动
,并且函数名称是模板的名称。 例如,上述示例依赖于此推导指南的std::array
:
namespace std { template <class T, class... U> array(T, U...) -> std::array<T, 1 + sizeof...(U)>; }Constructors in a primary template (as opposed to a template specialization) also implicitly define deduction guides.
当您声明依赖于 CTAD 的变量时,编译器将使用构造函数重载解析规则选择推导指南,该指南的返回类型将成为变量的类型。
CTAD 有时允许您从代码中省略样板。
从构造函数生成的隐式推导指南可能具有不良行为,或者完全不正确。 对于在 CTAD C++17 中引入之前编写的构造函数来说,这尤其成问题,因为这些构造函数的作者无法知道(更不是修复)其构造函数会给 CTAD 造成的任何问题。 此外,添加显式演绎指南以解决这些问题可能会破坏依赖于隐式演绎指南的任何现有代码。
CTAD 也存在许多与自动
相同的缺点,因为它们都是从初始化器中推断变量类型全部或部分的机制。 CTAD确实给读者更多的信息比汽车
,但它也没有给读者一个明显的提示,信息已被省略。
不要将 CTAD 与给定模板一起使用,除非模板的维护者选择通过提供至少一个显式演绎指南来支持 CTAD 的使用(并且假定 std
命名空间中的所有模板都已选择加入)。 如果可用,应使用编译器警告强制执行。
CTAD的使用也必须遵守关于类型扣除的一般规则。
Lambda 表达式
在适当情况下使用 lambda 表达式。 首选显式捕获 lambda 将转义当前作用域时。
Lambda 表达式是创建匿名函数对象的简明方法。 当将函数作为参数传递时,它们通常很有用。 例如:
std::sort(v.begin(), v.end(), [](int x, int y) { return Weight(x) < Weight(y); });
它们进一步允许从封闭作用域中显式按名称捕获变量,或者隐式使用默认捕获。 显式捕获要求列出每个变量,作为值或引用捕获:
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; });
默认捕获隐式捕获 lambda 正文中引用的任何变量,包括使用任何成员时:
const std::vector<int> lookup_table = ...; std::vector<int> indices = ...; // Captures `lookup_table` by reference, sorts `indices` by the value // of the associated element in `lookup_table`. std::sort(indices.begin(), indices.end(), [&](int a, int b) { return lookup_table[a] < lookup_table[b]; });
变量捕获还可以具有显式初始化程序,可用于按值捕获仅移动变量,或用于普通引用或值捕获未处理的其他情况:
std::unique_ptr<Foo> foo = ...; [foo = std::move(foo)] () { ... }Such captures (often called "init captures" or "generalized lambda captures") need not actually "capture" anything from the enclosing scope, or even have a name from the enclosing scope; this syntax is a fully general way to define members of a lambda object:
[foo = std::vector<int>({1, 2, 3})] () { ... }The type of a capture with an initializer is deduced using the same rules as
auto
.
模板元编程
避免复杂的模板编程。
模板元编程是指利用C++模板实例化机制是图灵完成,可用于在类型域中执行任意编译时间计算这一事实的一系列技术。
模板元编程允许极其灵活的接口,类型安全且高性能。 像谷歌测试,std::tuple,std::
功能
,和提升等设施。没有它,精神是不可能的。
除了语言专家,模板元编程中使用的技术通常对任何人都是模糊不清的。 以复杂方式使用模板的代码通常无法读取,并且很难调试或维护。
模板元编程通常会导致极差的编译时间错误消息:即使接口很简单,当用户做错事时,复杂的实现细节也会变得可见。
模板元编程使重构工具的工作更加困难,从而干扰了大规模重构。 首先,模板代码在多个上下文中展开,并且很难验证转换在所有上下文中是否有意义。 其次,某些重构工具使用 AST,该 AST 仅在模板扩展后表示代码的结构。 很难自动工作回需要重写的原始源构造。
模板元编程有时允许比没有它更干净、更易于使用的接口,但它也经常是一种过于聪明的诱惑。 它最好用于少数低级组件,这些组件的额外维护负担分散在大量用途中。
使用模板元编程或其他复杂的模板技术之前,要三思而行;考虑团队的普通成员在切换到其他项目后是否能够很好地理解您的代码来维护代码,或者非C++程序员或随意浏览代码库的人是否能够理解错误消息或跟踪要调用的函数的流。 如果您使用的是递归模板实例化或类型列表或元函数或表达式模板,或者依赖于 SFINAE 或检测函数重载分辨率的sizeof
技巧,那么您很可能走得太远了。
如果使用模板元编程,您应该会投入大量精力来最小化和隔离复杂性。 应尽可能将元编程隐藏为实现详细信息,以便面向用户的标头是可读的,并且应确保对棘手的代码进行特别良好的注释。 您应该仔细记录代码的使用方式,并且应该说明"生成的"代码是什么样子。 特别注意编译器在用户出错时发出的错误消息。 错误消息是用户界面的一部分,应根据需要调整代码,以便从用户的角度来看,错误消息是可理解和操作的。
Boost
仅使用"提升"库集合中已批准的库。
Boost 库集合是同行评审、免费、开源C++库的流行集合。
升压码通常质量很高,可携带性广泛,填补了C++标准库中的许多重要空白,如类型特征和更好的活页夹。
某些 Boost 库鼓励可能妨碍可读性的编码实践,例如元编程和其他高级模板技术,以及过于"功能"的编程风格。
为了保持所有可能读取和维护代码的参与者的高可读性,我们只允许批准 Boost 功能子集。 目前,允许以下库:
我们正在积极考虑将其他 Boost 功能添加到列表中,因此将来可能会扩展此列表。
std::hash
不要定义std::哈希
的专门化。
std::hash_lt;T>
是 C++11 哈希容器用于哈希类型T
的键的函数对象,除非用户显式指定不同的哈希函数。 例如,std:unordered_map<int,std::string>
是使用std::哈希<int>
哈希其键的哈希映射,而std::unordered_map<int,std:::string,MyIntHash>
使用MyIntHash。
std::哈希
为所有积分、浮点、指针和枚举
类型以及一些标准库类型(如字符串
和unique_ptr)
定义。 用户可以通过为这些类型的类型定义其专门化,使其能够针对自己的类型工作。
std::哈希
易于使用,并且简化了代码,因为您不必显式命名它。 专业std::哈希
是指定如何对类型进行哈希处理的标准方法,因此它是外部资源将教授的内容,以及新工程师的期望。
st::哈希
很难专门化。 它需要大量的样板代码,更重要的是,它将识别哈希输入的责任与执行哈希算法本身的责任结合起来。 类型作者必须负责前者,但后者需要类型作者通常没有且不需要的专业知识。 这里的利害关系很大,因为低质量哈希函数可能是安全漏洞,因为哈希洪水攻击的出现。
即使对于专家来说,std::哈希
专门化对于复合类型也很难正确实现,因为实现不能递归地调用std::哈希
的数据成员。 高质量的哈希算法可维护大量内部状态,并将该状态减少到std::哈希
返回的size_t
字节通常是计算中最慢的部分,因此不应执行多次。
由于正是这个问题,st:::哈希
不适用于std::pair
或 std:::元
组,并且语言不允许我们扩展它来支持它们。
您可以使用std::哈希
使用它支持的"开箱即用"的类型,但不专门化它以支持其他类型。
如果需要键类型的哈希表,则不支持:哈希,请考虑现在使用旧哈希容器(例如hash_map);如果哈希表的类型为std::哈希
,请考虑使用旧哈希容器(例如hash_map)。
它们使用不同的默认哈希her,不受此禁止的影响。
如果要使用标准哈希容器,则需要为密钥类型指定自定义哈希,例如。
std::unordered_map<MyKeyType, Value, MyKeyTypeHasher> my_map;
请与类型的所有者联系,查看是否有可以使用的现有哈希HER;例如,可以使用该哈希处理。否则与他们合作,提供一个,或滚动自己的。
我们计划提供一个哈希函数,可以使用新的自定义机制,没有std::哈希
的缺点。
其他C++功能
与Boost一样,一些现代C++扩展鼓励妨碍可读性的编码实践,例如删除可能对读者有帮助的已检查冗余(如类型名称),或者鼓励模板元编程。 其他扩展重复现有机制提供的功能,这可能导致混淆和转换成本。
除了样式指南的其余部分所述之外,不得使用以下C++功能:
非标准扩展
除非另有说明,否则不得使用非标准扩展至C++。
编译器支持不是标准C++一部分的各种扩展。 此类扩展包括 GCC 的__attribute__、
内在函数(如__builtin_prefetch、
指定初始化器(例如 Foo f = [.field = 3])、
内联程序集、__COUNTER__、__PRETTY_FUNCTION__、
复合语句表达式(例如
foo = (= int x;条形(&x);x =),
可变长度数组和分配()
和"猫王运算符"
a?:b
.
请勿使用非标准扩展。 您可以使用使用非标准扩展实现的可移植性包装,只要这些包装由指定的项目范围可移植性标头提供。
别名
公共别名有利于 API 用户,应明确记录。
有几种方法可以创建作为其他实体别名的名称:
typedef Foo Bar; using Bar = Foo; using other_namespace::Foo;
在新代码中,使用
比typedef
更可取,因为它提供了与C++的其余部分更一致的语法,并且适用于模板。
与其他声明一样,在头文件中声明的别名是该标头的公共 API 的一部分,除非它们位于函数定义、类的私有部分或显式标记的内部命名空间中。 此类区域或 .cc 文件中的别名是实现详细信息(因为客户端代码不能引用它们),并且不受此规则的限制。
不要在公共 API 中放置别名,只是为了保存在实现中的键入内容;除非在实现中输入别名。仅当您希望客户端使用它时,才执行此操作。
定义公共别名时,记录新名称的意图,包括是否保证它始终与当前别名的类型相同,或者是否打算使用更有限的兼容性。 这使用户知道是否可以将类型视为可更改类型,或者是否必须遵循更具体的规则,并可帮助实现保留一定程度的更改别名的自由。
不要在公共 API 中放置命名空间别名。 (另请参阅命名空间)。
例如,这些别名记录它们在客户端代码中的使用方式:
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
这些别名不记录预期用途,其中一半不适合客户端使用:
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 文件中都很好:
// In a .cc file using foo::Bar;
命名约定
最重要的一致性规则是控制命名的规则。 命名的风格会立即告诉我们命名实体的类型、变量、函数、常量、宏等,而无需我们搜索该实体的声明。 我们大脑中的模式匹配引擎在很大程度上依赖于这些命名规则。
命名规则是非常武断的,但我们认为一致性比个人偏好在这一领域更重要,所以不管你是否认为这些规则是明智的,规则就是规则。
通用命名规则
使用名称优化可读性,这些名称即使对不同团队中的人也清晰可见。
使用描述对象用途或意图的名称。
不要担心节省水平空间,因为让新读取器立即可以理解您的代码更为重要。 尽量减少使用项目外部的人可能未知的缩写(尤其是首字母缩略词和首字母缩写)。 不要通过删除单词中的字母来缩写。 根据经验,如果缩写列在维基百科中,那么这个缩写可能没问题。 一般来说,描述性应与名称的可见性范围成正比。 例如,n
可能是 5 行函数中的一个精细名称,但在类范围内,它可能过于模糊。
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 };
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。
对于下面的命名规则,"单词"是任何您用英文书写的,没有内部空格。 这包括缩写和首字母缩略词;例如,对于"骆驼案例"或"帕斯卡案",其中每个单词的第一个字母被大写,使用名称如StartRpc()
,而不是StartRPC()。
模板参数应遵循其类别的命名样式:类型模板参数应遵循类型名称的规则,非类型模板参数应遵循变量名称的规则。
文件命名
文件名应为小写,可以包含下划线 (*
) 或破折号 (-
)。
遵循项目使用的约定。 如果没有一致的本地模式可遵循,则选择"*"。
可接受的文件名示例:
C++文件应以.cc
结尾,头文件应以.h 结尾
。 依赖于在特定点的文本包含的文件应以.inc
结尾(另请参阅有关自包含标头的部分)。
不要使用在 /usr/include
中已经存在的文件名,例如db.h
。
通常,使文件名非常具体。 例如,使用http_server_logs.h
而不是日志。
一个非常常见的情况是有一对文件称为foo_bar.h
和foo_bar.cc,
定义一个叫做FooBar
的类。
类型命名
键入名称以大写字母开头,每个新单词都有大写字母,没有下划线:我的兴奋类
,我的兴奋名
。
所有类型的名称(类、结构、类型别名、枚举和类型模板参数)具有相同的命名约定。 类型名称应以大写字母开头,每个新单词都应有一个大写字母。 没有下划线。 例如:
// 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_variable、a_struct_data_member、a_class_data_member_。
普通变量命名
例如:
std::string table_name; // OK - lowercase with underscore.
std::string tableName; // Bad - mixed case.
类数据成员
类的数据成员(静态和非静态)与普通非成员变量一样命名,但使用尾随下划线。
class TableInfo { ... private: std::string table_name_; // OK - underscore at end. static Pool<TableInfo>* pool_; // OK. };
结构体数据成员
结构的数据成员(静态和非静态)与普通非成员变量一样命名。 它们没有类中的数据成员具有的尾随下划线。
struct UrlTableProperties { std::string name; int num_entries; static Pool<UrlTableProperties>* pool; };
看到 结构与 类 讨论何时使用结构与类。
常量命名
变量声明的 constexpr 或 const,其值在程序的持续期间是固定的,用前导"k"命名,后跟混合大小写。 在不能将大写用于分离的极少数情况下,下划线可用作分隔符。 例如:
const int kDaysInAWeek = 7; const int kAndroid8_0_0 = 24; // Android 8.0.0
所有具有静态存储持续时间(即静态和全局变量,请参阅存储持续时间以了解详细信息)的此类变量应以这种方式命名。 此约定对于其他存储类的变量(例如自动变量)是可选的,否则将应用通常的变量命名规则。
函数命名
常规函数有混合情况;访问器和突变器可以像变量一样命名。
通常,函数应以大写字母开头,每个新单词都应有一个大写字母。
AddTableEntry() DeleteUrl() OpenFileOrDie()
(相同的命名规则适用于作为 API 的一部分公开且外观像函数的类和命名空间范围常量,因为它们是对象而不是函数这一事实并不重要的实现细节。
访问器和突变器(获取和设置函数)可以像变量一样命名。 这些通常对应于实际的成员变量,但这不是必需的。 例如,int 计数()
和void set_count(int 计数)。
命名空间命名
命名空间名称都是小写。 顶级命名空间名称基于项目名称。 避免嵌套命名空间与众所周知的顶级命名空间之间的冲突。顶级命名空间的名称通常应为代码包含在该命名空间中的项目或团队的名称。 该命名空间中的代码通常应位于其基名与命名空间名称匹配的目录中(或其子目录中)。
请记住,针对缩写名称的规则适用于命名空间,就像变量名称一样。 命名空间内的代码很少需要提及命名空间名称,因此通常不需要特别的缩写。
避免使用与已知顶级命名空间匹配的嵌套命名空间。 由于名称查找规则,命名空间名称之间的冲突可能会导致令人惊讶的生成中断。 特别是,不要创建任何嵌套的 std
命名空间。 首选唯一的项目标识符(Web 搜索:索引
,web 搜索:index_util)
而不是容易发生冲突的名称,如Web 搜索::util
。
对于内部
命名空间,应警惕将其他代码添加到同一内部
命名空间导致冲突(团队中的内部帮助器往往相关,并可能导致冲突)。 在这种情况下,使用文件名创建唯一的内部名称是有帮助的(websearch::index:frobber_internal
用于frobber.h)
枚举命名
枚举器(对于作用域枚举和非范围枚举枚举)应像常量或宏一样命名:kEnumName
或ENUM_NAME
。
优选地,单个枚举器的命名应该像常量一样。 但是,将它们命名为宏也是可以接受的。 枚举名称UrlTableErrors(
和备用UrlTableErrors
)是一种类型,因此是混合大小写。
enum UrlTableErrors { kOk = 0, kErrorOutOfMemory, kErrorMalformedInput, }; enum AlternateUrlTableErrors { OK = 0, OUT_OF_MEMORY = 1, MALFORMED_INPUT = 2, };
直到2009年1月,这种风格是命名枚举值,如宏。 这会导致枚举值和宏之间的名称冲突问题。 因此,对首选常量样式命名的更改进行了实施。 如果可能,新代码应首选常量样式命名。 但是,没有理由将旧代码更改为使用常量样式的名称,除非旧名称实际上会导致编译时问题。
宏命名
你不会真正定义一个宏,是吗? 如果你这样做,他们是这样的:MY_MACRO_THAT_SCARES_SMALL_CHILDREN_AND_ADULTS_ALIKE。
请参阅宏的说明;通常不应使用宏。 但是,如果绝对需要它们,则应用所有大写字母和下划线命名它们。
#define ROUND(x) ... #define PI_ROUNDED 3.0
命名规则的特例
如果要命名类似于现有 C 或 C++ 实体的内容,则可以遵循现有的命名约定方案。
注释
注释对于保持代码的可读性至关重要。 以下规则描述了您应该注释的内容以及评论的位置。 但请记住:虽然注释非常重要,但最好的代码是自我记录。 为类型和变量提供合理名称比使用模糊名称好得多,然后必须通过注释来解释这些名称。
在撰写评论时,请为受众编写:需要了解代码的下一个贡献者。 慷慨 —— 下一个可能是你!
注释风格
使用//
或/* + / 语法
,只要您保持一致。
您可以使用//
或/* +/
语法;然而,//
是更常见的。 与评论方式以及使用的位置使用样式保持一致。
文件注释
使用许可证样板启动每个文件。
文件注释描述文件的内容。 如果文件声明、实现或测试声明、实现或测试声明时注释记录的一个抽象,则不需要文件注释。 所有其他文件必须具有文件注释。
法律公告和作者信息
每个文件都应包含许可证样板。 为项目使用的许可证选择适当的样板(例如,Apache 2.0、BSD、LGPL、GPL)。
如果对具有作者行的文件进行了重大更改,请考虑删除作者行。 新文件通常不应包含版权声明或作者行。
文件内容
如果.h
声明多个抽象,则文件级注释应大致描述文件的内容以及抽象的关系。 1 或 2 个句子的文件级注释可能就足够了。 有关各个抽象的详细文档属于这些抽象,而不是文件级别。
不要重复.h
和.cc
中的注释。 重复的注释有分歧。
类注释
每个非显而易见的类声明都应有一个附带注释,说明其用途以及如何使用它。
// Iterates over the contents of a GargantuanTable. // Example: // GargantuanTableIterator* iter = table->NewIterator(); // for (iter->Seek("foo"); !iter->done(); iter->Next()) { // process(iter->key(), iter->value()); // } // delete iter; class GargantuanTableIterator { ... };
类注释应为读者提供足够的信息,以便了解如何以及何时使用类,以及正确使用类所需的任何其他注意事项。 记录类所做的同步假设(如果有)。 如果类的实例可以由多个线程访问,请特别注意记录围绕多线程使用的规则和固定变量。
类注释通常是一个小示例代码片段的好地方,它演示了类的简单且重点突出的用法。
当充分分隔(例如.h
和.cc
文件)时,描述类使用情况的注释应与其接口定义一起使用;有关类操作和实现的评论应伴随类方法的实现。
函数注释
声明注释描述函数的使用(当它不是明显的);函数定义中的注释描述操作。
函数声明
几乎每个函数声明都应在它前面有注释来描述函数的作用和如何使用它。 仅当函数简单且显而易见(例如,类的明显属性的简单访问器)时,才能省略这些注释。 这些注释应以指示性情绪的描述性动词("打开文件")而不是命令中的动词("打开文件")打开。 注释描述函数;它不告诉函数该怎么做。 通常,这些注释不描述函数如何执行其任务。 相反,这应留给函数定义中的注释。
函数声明注释中要提及的事项类型:
下面是一个示例:
// 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;
但是,不要不必要地冗长或说明完全明显。
记录函数重写时,请关注重写本身的细节,而不是重复重写函数中的注释。 在许多情况下,重写不需要额外的文档,因此无需注释。
注释构造函数和析构函数时,请记住,读取代码的人知道构造函数和析构函数的用途,因此仅说"销毁此对象"之类的注释是无用的。 记录构造函数如何处理其参数(例如,如果它们拥有指针的所有权),以及析构函数执行哪些清理操作。 如果这是微不足道的,只需跳过注释。 析构函数没有标头注释是很常见的。
函数定义
如果函数如何执行其工作有任何棘手问题,则函数定义应该有一个解释性注释。 例如,在定义注释中,您可以描述您使用的任何编码技巧,概述您要完成的步骤,或者解释为什么选择以您的方式实现函数,而不是使用可行的替代方法。 例如,您可能会提到为什么它必须获取函数前半部分的锁,但为什么下半年不需要锁。
请注意,不应在.h
文件中或任何地方重复函数声明给出的注释。 可以简要地概括函数的功能,但注释的重点应放在它如何执行上。
变量注释
通常,变量的实际名称应具有足够的描述性,以便很好地说明变量的用途。 在某些情况下,需要更多的注释。
类数据成员
每个类数据成员(也称为实例变量或成员变量)的用途必须明确。 如果类型和名称没有明确表示任何不变性(特殊值、成员之间的关系、生存期要求),则必须注释它们。 但是,如果类型和名称足够(int num_events_;
),则无需注释。
特别是,添加注释来描述哨兵值的存在和意义,例如空点值或 -1,当它们不明显时。 例如:
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_;
全局变量
所有全局变量都应有注释,描述它们是什么、它们用于什么以及(如果不清楚)为什么它需要是全局变量。 例如:
// The total number of tests cases that we run through in this regression test. const int kNumTestCases = 6;
代码实现中的注释
在你的代码实现中,你应该在代码中巧妙的、 晦涩的、有趣的、重要的地方加以注释。
解释性评论
巧妙或复杂的代码块前面应有注释。 例子:
// 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 个空格。 例子:
// 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.
请注意,有两个注释描述代码正在执行的操作,以及提到函数返回时已记录错误的注释。
函数参数注释
当函数参数的含义不明显时,请考虑以下补救措施之一:
// What are these arguments? const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);
与:
ProductOptions options; options.set_precision_decimals(7); options.set_use_cache(ProductOptions::kDontUseCache); const DecimalNumber product = CalculateProduct(values, options, /*completion_callback=*/nullptr);
不允许的行为
不要说显而易见的。 特别是,不要从字面上描述代码的作用,除非行为对于理解C++的读者来说并不明显。 相反,提供更高级别的注释来描述代码执行它的作用的原因,或者使代码自我描述。
比较:// Find the element in the vector. <-- Bad: obvious! auto iter = std::find(v.begin(), v.end(), element); if (iter != v.end()) { Process(element); }为此:
// Process "element" unless it was already processed. auto iter = std::find(v.begin(), v.end(), element); if (iter != v.end()) { Process(element); }自描述代码不需要注释。 The comment from the example above would be obvious:
if (!IsAlreadyProcessed(element)) { Process(element); }
标点、拼写和语法
注意标点符号、拼写和语法;阅读写得好的评论比写得不好的评论容易。
注释应与叙述文本一样可读,具有适当的大写和标点符号。 在许多情况下,完整的句子比句子片段更具可读性。 较短的注释(如代码行末尾的注释)有时可能不太正式,但您应该与样式保持一致。
尽管让代码审阅者指出在应该使用分号时使用逗号可能令人沮丧,但源代码保持高度的清晰度和可读性非常重要。 正确的标点符号、拼写和语法有助于实现这一目标。
TODO 注释
将TODO
注释用于临时代码、短期解决方案或足够好但并非完美的代码。
TODO
s 应在所有大写字母中包含字符串TODO,
后跟人员的姓名、电子邮件地址、错误 ID 或其他标识符,或者有关TODO
引用的问题的最佳上下文问题。 主要目的是有一个一致的TODO,
可以搜索,以了解如何根据要求获得更多的细节。 TODO
不是被引用的人员将解决问题的承诺。 因此,当您创建具有名称的TODO
时,几乎总是您指定的名称。
// 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
的格式为"将来执行某些操作",请确保包含非常具体的日期("2005 年 11 月修复")或非常具体的事件("当所有客户端都可以处理 XML 响应时删除此代码")。
格式
编码样式和格式非常随意,但如果每个人都使用相同的样式,则项目更容易遵循。 个人可能不同意格式规则的各个方面,并且某些规则可能需要一些习惯,但重要的是,所有项目参与者都遵循样式规则,以便所有人都能轻松阅读和理解每个人的代码。
为了帮助您正确设置代码格式,我们为emacs创建了一个设置文件。
行长度
代码中的每行文本最多应为 80 个字符。
我们认识到,这条规则是有争议的,但很多现有的代码已经遵守它,我们认为一致性是重要的。
赞成这条规则的人认为,强迫他们调整窗户大小是不礼貌的,没有必要再这样了。 有些人习惯于并排有多个代码窗口,因此在任何情况下都没有空间来扩展窗口。 人们设置工作环境时假定具有特定的最大窗口宽度,并且 80 列已成为传统标准。 为什么要改变它?
变革的支持者认为,更宽的行可以使代码更具可读性。 80 列的限制是回溯到 20 世纪 60 年代的大型机;现代设备有宽的屏幕,可以很容易地显示更长的线条。
80 个字符是最大值。
一行可能超过 80 个字符(如果是
非 ASCII 字符
非 ASCII 字符应该很少,并且必须使用 UTF-8 格式。
不应在源(甚至英语)中硬编码面向用户的文本,因此使用非 ASCII 字符的情况应该很少。 但是,在某些情况下,在代码中包含此类单词是适当的。 例如,如果代码解析来自外源的数据文件,则对这些数据文件中使用的非 ASCII 字符串进行硬编码可能适合作为分隔符。 更常见的情况是,单位测试代码(不需要本地化)可能包含非 ASCII 字符串。 在这种情况下,您应该使用 UTF-8,因为这是大多数工具所理解的编码,它不仅仅能够处理 ASCII。
十六进制编码也很好,并且鼓励它提高可读性的地方 - 例如,"[xEF_xBB_xBF],
或者,更简单地说,u8"\uFEFF",
是Unicode零宽度无中断空格字符,如果作为直 UTF-8 包含在源中,该字符将不可见。
使用u8
前缀可保证包含\uXXXX
转义序列的字符串文本编码为 UTF-8。
不要将其用于包含编码为 UTF-8 的非 ASCII 字符的字符串,因为如果编译器不将源文件解释为 UTF-8,这将产生不正确的输出。
不应使用C++11char16_t和char32_t
字符类型,因为它们适用于非 UTF-8 文本。 出于类似的原因,您也不应使用
wchar_t(
除非您编写的代码与 Windows API 交互,该 API 广泛使用wchar_t)。
空格 vs. 制表符
一次仅使用空格和缩进 2 个空格。
我们使用空格进行缩进。 请勿在代码中使用选项卡。 应将编辑器设置为在点击制表键时发出空格。
函数声明和定义
返回与函数名称相同的行的类型,如果参数适合,则返回同一行上的参数。 包装参数列表,这些列表不适合在单行上,就像在函数调用中包装参数一样。
函数如下所示:
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) { DoSomething(); ... }
如果文本太多,无法容纳在一行上:
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2, Type par_name3) { DoSomething(); ... }
或者,如果您连第一个参数都不适合:
ReturnType LongClassName::ReallyReallyReallyLongFunctionName( Type par_name1, // 4 space indent Type par_name2, Type par_name3) { DoSomething(); // 2 space indent ... }
需要注意的一些要点:
可以省略从上下文中明显显示的未使用的参数:
class Foo { public: Foo(const Foo&) = delete; Foo& operator=(const Foo&) = delete; };
可能不太明显的未使用的参数应注释掉函数定义中的变量名称:
class Shape { public: virtual void Rotate(double radians) = 0; }; class Circle : public Shape { public: void Rotate(double radians) override; }; void Circle::Rotate(double /*radians*/) {}
// Bad - if someone wants to implement later, it's not clear what the // variable means. void Circle::Rotate(double) {}
扩展为属性的属性和宏显示在函数声明或定义的最开始,在返回类型之前:
ABSL_MUST_USE_RESULT bool IsOk();
Lambda 表达式
设置参数和实体的格式,如对任何其他函数进行格式化,并捕获列表,如其他逗号分隔列表。
对于按引用捕获,请勿在安培和变量名称之间留一个空格。
int x = 0; auto x_plus_n = [&x](int n) -> int { return x + n; }
短羔羊可以内联作为函数参数编写。
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
不会被误认为是十六进制数字。 使用整数文本初始化浮点变量(假设变量类型可以精确表示该整数)是可以的,但请注意,指数表示法中的数字从来不是整数文本。
float f = 1.f; long double ld = -.5L; double d = 1248e6;
float f = 1.0f; float f2 = 1; // Also OK long double ld = -0.5L; double d = 1248.0e6;
函数调用
要么将调用全部写在一行上,在括号处包装参数,或者在缩进四个空格的新行上启动参数,然后继续在 4 个空格缩进处继续。 在没有其他考虑的情况下,使用最小行数,包括在每个行上放置多个参数(如果适用)。
函数调用具有以下格式:
bool result = DoSomething(argument1, argument2, argument3);
如果参数不全部适合一行,则应将它们分解到多行上,每个后续行与第一个参数对齐。 请勿在打开的 paren 后或关闭 paren 之前添加空格:
bool result = DoSomething(averyveryveryverylongargument1, argument2, argument3);
参数可以选择全部放在具有四个空格缩进的后续行上:
if (...) { ... ... if (...) { bool result = DoSomething( argument1, argument2, // 4 space indent argument3, argument4); ... }
在一行上放置多个参数,以减少调用函数所需的行数,除非存在特定的可读性问题。 有些人发现,在每行上严格使用一个参数的格式设置更具可读性,并且简化了参数的编辑。 但是,我们优先于编辑参数的易用性,并且大多数可读性问题都通过以下技术更好地解决。
如果由于构成某些参数的表达式的复杂性或混淆性而使单个行中的多个参数降低可读性,请尝试创建以描述性名称捕获这些参数的变量:
int my_heuristic = scores[x] * y + bases[x]; bool result = DoSomething(my_heuristic, x, y, z);
或者用解释性注释将令人困惑的论点放在其自身行中:
bool result = DoSomething(scores[x] * y + bases[x], // Score heuristic. x, y, z);
如果仍然存在一个参数在其自己的行上可读性显著增强的情况,则将其放在自己的行上。 决策应特定于更可读性而非一般策略的参数。
有时,参数形成一个对可读性很重要的结构。 在这些情况下,请随意根据该结构格式化参数:
// Transform the widget by a 3x3 matrix. my_widget.Transform(x1, x2, x3, y1, y2, y3, z1, z2, z3);
花括号初始化列表格式
设置大括号初始化程序列表的格式,就像将函数调用格式化一样。
如果大括号列表遵循名称(例如类型或变量名称),则格式与*
一样是具有该名称的函数调用的括号。 如果没有名称,则假定名称为零长度。
// Examples of braced init list on a single line. return {foo, bar}; functioncall({foo, bar}); std::pair<int, int> p{foo, bar}; // When you have to wrap. 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}};
条件语句
最好在括号内没有空格。 if
和else
关键字属于单独的行。
基本条件语句有两种可接受的格式。 一个包括括号和条件之间的空格,一个不包括。
最常见的形式是无空格。 要么是好的,但保持一致。 如果要修改文件,请使用已存在的格式。 如果要编写新代码,请使用该目录中的其他文件或项目中使用格式。 如果有疑问,并且您没有个人偏好,请不要添加空格。
if (condition) { // no spaces inside parentheses ... // 2 space indent. } else if (...) { // The else goes on the same line as the closing brace. ... } else { ... }
如果您愿意,您可以在括号内添加空格:
if ( condition ) { // spaces inside parentheses - rare ... // 2 space indent. } else { // The else goes on the same line as the closing brace. ... }
请注意,在所有情况下,都必须在if
和打开的括号之间有一个空格。 如果使用括号,则必须在右括号和大括号之间有一个空格。
if(condition) { // Bad - space missing after IF. if (condition){ // Bad - space missing before {. if(condition){ // Doubly bad.
if (condition) { // Good - proper space after IF and before {.
如果这增强了可读性,则可以在一行上写入短条件语句。 仅当行简短且语句不使用else
子句时,才可以使用此选项。
if (x == kFoo) return new Foo(); if (x == kBar) return new Bar();
当 if 语句具有其他
: 时不允许这样做:
// Not allowed - IF statement on one line when there is an ELSE clause if (x) DoThis(); else DoThat();
通常,单行语句不需要大括号,但如果您喜欢它们,则允许它们大括号;具有复杂条件或语句的条件语句或循环语句可能使用大括号更具可读性。 某些项目要求if
必须始终具有随附的大括号。
if (condition) DoSomething(); // 2 space indent. if (condition) { DoSomething(); // 2 space indent. }
但是,如果if
-else
语句的一部分使用大括号,则另一部分也必须:
// Not allowed - curly on IF but not ELSE if (condition) { foo; } else bar; // Not allowed - curly on ELSE but not IF if (condition) foo; else { bar; }
// Curly braces around both IF and ELSE required because // one of the clauses used braces. if (condition) { foo; } else { bar; }
循环和 Switch 语句
开关语句可能使用大括号表示块。 在案例之间对非平凡的跌落进行区分。
大括号是单语句循环的可选选项。
空循环体应使用空大括号或继续
。
switch
语句中的案例
块可以有大括号或不大括号,具体取决于您的偏好。 如果包含大括号,则应将其放置如下图所示。
如果不以枚举值为条件,switch 语句应始终具有默认值
(对于枚举值,编译器将警告您,如果未处理任何值)。 如果不应执行默认情况,请视为错误。 例如:
switch (var) { case 0: { // 2 space indent ... // 4 space indent break; } case 1: { ... break; } default: { assert(false); } }
必须使用ABSL_FALLTHROUGH_INTENDED(
宏(在absl/base/macro.h
中定义)对从一个案例标签到另一个案例标签进行标注。
ABSL_FALLTHROUGH_INTENDED;
应放置在发生下一个案例标签的执行点。 常见的例外是连续大小写标签而不干预代码,在这种情况下不需要注释。
switch (x) { case 41: // No annotation needed here. case 43: if (dont_be_picky) { // Use this instead of or along with annotations in comments. ABSL_FALLTHROUGH_INTENDED; } else { CloseButNoCigar(); break; } case 42: DoSomethingSpecial(); ABSL_FALLTHROUGH_INTENDED; default: DoSomethingGeneric(); break; }
大括号是单语句循环的可选选项。
for (int i = 0; i < kSomeNumber; ++i) printf("I love you\n"); for (int i = 0; i < kSomeNumber; ++i) { printf("I take it back\n"); }
空循环实体应使用空大括号对,或继续
不使用大括号,而不是单个分号。
while (condition) { // Repeat test until it returns false. } for (int i = 0; i < kSomeNumber; ++i) {} // Good - one newline is also OK. while (condition) continue; // Good - continue indicates no logic.
while (condition); // Bad - looks like part of do/while loop.
指针和引用表达式
期间或箭头周围没有空格。 指针运算符没有尾随空格。
以下是格式正确的指针和引用表达式的示例:
x = *p; p = &x; x = r.y; x = r->y;
请注意:
声明指针变量或参数时,可以将星号放在类型或变量名称的旁边:
// These are fine, space preceding. char *c; const std::string &str; // These are fine, space following. char* c; const std::string& str;
应在单个文件中一致地执行此操作,因此,在修改现有文件时,请使用该文件中的样式。
允许(如果异常)在同一声明中声明多个变量,但如果其中任何变量具有指针或引用修饰,则不允许这样做。 这种声明很容易被误读。// Fine if helpful for readability. int x, y;
int x, *y; // Disallowed - no & or * in multiple declaration char * c; // Bad - spaces on both sides of * const std::string & str; // Bad - spaces on both sides of &
布尔表达式
当您的布尔表达式长度大于标准行长度时,在如何断开行时要保持一致。
在此示例中,逻辑 AND 运算符始终位于行的末尾:
if (this_one_thing > this_other_thing && a_third_thing == a_fourth_thing && yet_another && last_one) { ... }
请注意,当代码在本示例中换行时,两个&
和逻辑 AND 运算符都在行的末尾。 这在 Google 代码中更为常见,但允许在行的开头包装所有运算符。 请随意明智地插入额外的括号,因为它们在适当使用时对提高可读性很有帮助。 另请注意,应始终使用标点运算符,如&
和*,
而不是单词运算符,如和
和compl
。
返回值
不要不必要地用括号括起返回
表达式。
在return expr
中使用括号;仅在您将在x = expr
中使用它们的地方;。
return result; // No parentheses in the simple case. // Parentheses OK to make a complex expression more readable. return (some_long_condition && another_condition);
return (value); // You wouldn't write var = (value);
return(result); // return is not a function!
变量和数组初始化
你可以选择 =
、()
或 {}
。
你可以选择=
、()
和 {}
;以下方式都是正确的:
int x = 3; int x(3); int x{3}; std::string name = "Some Name"; std::string name("Some Name"); std::string name{"Some Name"};
在具有std::initializer_list
构造函数的类型上使用大括号初始化列表[...]
时要小心。
非空括号 init 列表尽可能首选std::initializer_list
构造函数。 请注意,空大括号=
是特殊的,如果可用,将调用默认构造函数。 要强制非std:initializer_list
构造函数,请使用括号而不是大括号。
std::vector<int> v(100, 1); // A vector containing 100 items: All 1s. std::vector<int> v{100, 1}; // A vector containing 2 items: 100 and 1.
此外,大括号形式可防止积分类型变窄。 这可以防止某些类型的编程错误。
int pi(3.14); // OK -- pi == 3.
int pi{3.14}; // Compile error: narrowing conversion.
预处理指令
启动预处理器指令的哈希标记应始终位于行的开头。
即使预处理器指令位于缩进代码的正文中,指令也应从行的开头开始。
// Good - directives at beginning of line if (lopsided_score) { #if DISASTER_PENDING // Correct -- Starts at beginning of line DropEverything(); # if NOTIFY // OK but not required -- Spaces after # NotifyClient(); # endif #endif BackToNormal(); }
// Bad - indented directives if (lopsided_score) { #if DISASTER_PENDING // Wrong! The "#if" should be at beginning of line DropEverything(); #endif // Wrong! Do not indent "#endif" BackToNormal(); }
类格式
各部分顺序为public
、protected
和 private
, 每个部分缩进 1 个空格。
类定义的基本格式(缺少注释,请参阅类注释来讨论需要哪些注释)是:
class MyClass : public OtherClass { public: // Note the 1 space indent! MyClass(); // Regular 2 space indent. 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_; };
需要注意的事项:
构造函数初始程序列表
构造函数初始化程序列表可以全部位于一行上,也可以用后续行缩进四个空格。
初始化程序列表的可接受格式包括:
// When everything fits on one line: MyClass::MyClass(int var) : some_var_(var) { DoSomething(); } // If the signature and initializer list are not all on one line, // you must wrap before the colon and indent 4 spaces: MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) { DoSomething(); } // When the list spans multiple lines, put each member on its own line // and align them: MyClass::MyClass(int var) : some_var_(var), // 4 space indent some_other_var_(var + 1) { // lined up DoSomething(); } // As with any other code block, the close curly can be on the same // line as the open curly, if it fits. MyClass::MyClass(int var) : some_var_(var) {}
命名空间格式
命名空间的内容不缩进。
命名空间不会添加额外的缩进级别。 例如,使用:
namespace { void foo() { // Correct. No extra indentation within namespace. ... } } // namespace
不要缩进命名空间:
namespace { // Wrong! Indented when it should not be. void foo() { ... } } // namespace
声明嵌套命名空间时,将每个命名空间放在自己的行上。
namespace foo { namespace bar {
水平空白
水平空白的使用取决于位置。 切勿在行尾放置尾随空格。
通用
void f(bool b) { // Open braces should always have a space before them. ... int i = 0; // Semicolons usually have no space before them. // Spaces inside braces for braced-init-list are optional. If you use them, // put them on both sides! int x[] = { 0 }; int x[] = {0}; // Spaces around the colon in inheritance and initializer lists. class Foo : public Bar { public: // For inline function implementations, put spaces between the braces // and the implementation itself. Foo(int b) : Bar(), baz_(b) {} // No spaces inside empty braces. void Reset() { baz_ = 0; } // Spaces separating braces from implementation. ...
添加尾随空格会给编辑同一文件的其他人带来额外的工作,当他们合并时,删除现有的尾随空格也会增加。 所以:不要引入尾随空格。 如果您已经更改了该行,或者在单独的清理操作中删除它(最好是在没有其他人正在处理该文件时)。
循环和条件
if (b) { // Space after the keyword in conditions and loops. } else { // Spaces around else. } while (test) {} // There is usually no space inside parentheses. switch (i) { for (int i = 0; i < 5; ++i) { // Loops and conditions may have spaces inside parentheses, but this // is rare. Be consistent. switch ( i ) { if ( test ) { for ( int i = 0; i < 5; ++i ) { // For loops always have a space after the semicolon. They may have a space // before the semicolon, but this is rare. for ( ; i < 5 ; ++i) { ... // Range-based for loops always have a space before and after the colon. for (auto x : counts) { ... } switch (i) { case 1: // No space before colon in a switch case. ... case 2: break; // Use a space after a colon if there's code after it.
运算符
// Assignment operators always have spaces around them. x = 0; // Other binary operators usually have spaces around them, but it's // OK to remove spaces around factors. Parentheses should have no // internal padding. v = w * x + y / z; v = w*x + y/z; v = w * (x + z); // No spaces separating unary operators and their arguments. x = -5; ++x; if (x && !y) ...
模板和强制转换
// No spaces inside the angle brackets (< and >), before // <, or between >( in a cast std::vector<std::string> x; y = static_cast<char*>(x); // Spaces between type and pointer are OK, but be consistent. std::vector<char *> x;
垂直空白
尽量减少使用垂直空白。
这更像是一个原则,而不是一个规则:当你不需要使用白行时,不要使用空白行。 特别是,不要在函数之间放置一个或两个空白行,拒绝用空行启动函数,不要用空行结束函数,并且保留使用空白行。 代码块中的空行就像散文中的段落中断:在视觉上分离两种思想。
基本原则是:在一个屏幕上安装的代码越多,就越容易遵循和理解程序的控制流。 故意使用空白来提供该流中的分离。
当空白行可能很有用时,一些经验法则可以帮助:
规则的特例
上述编码约定是强制性的。 但是,与所有好规则一样,这些规则有时也有例外,我们在此讨论这些异常。
已存在的不合格代码
在处理不符合此样式指南的代码时,可能会偏离规则。
如果您发现自己正在修改写入本指南所介绍规范以外的规范的代码,则可能必须偏离这些规则,才能与该代码中的本地约定保持一致。 如果您对如何执行此操作有疑问,请询问原始作者或当前负责代码的人员。 请记住,一致性也包括本地一致性。
Windows 代码
Windows 程序员已经开发了他们自己的编码约定集,主要来源于 Windows 标头和其他 Microsoft 代码中的约定。 我们希望让任何人轻松理解您的代码,因此,我们为在任意平台上编写C++的每个人提供了一套指南。
值得重申一些您可能忘记的准则,如果您习惯于流行的 Windows 样式:
但是,我们偶尔需要在 Windows 上中断一些规则:
临别赠言
运用常识并保持一致。
如果要编辑代码,请花几分钟时间查看周围的代码并确定其风格。 如果他们在if
子句周围使用空格,那么你也应该使用空格。 如果他们的评论周围有小盒的星星,让你的评论周围也小盒星星。
制定风格指南的目的是要有一个通用的编码规范,以便人们可以专注于你在说什么而不是你是如何说的。 我们在这里呈现整体风格规则,让人们知道这些规范。 但是当前风格也很重要。 如果添加到文件中的代码看起来与文件周围的现有代码大不相同,则不连续性会使读者在阅读时失去其节奏。 尽量避免这种情况。
好了,关于编码风格写的够多了;代码本身更有趣。 玩得愉快!