编码风格之(6)C++命名规范(Google风格)
Author: Once Day Date: 2024年10月9日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
本文档翻译自《Google C++ Style Guide》,仅供学习参考!
全系列文章可参考专栏: 源码分析_Once-Day的博客-****博客
参考文章:
- 67. 二进制求和 - 力扣(LeetCode)
- 力扣 (LeetCode) 全球极客挚爱的技术成长平台
文章目录
- 编码风格之(6)C++命名规范(Google风格)
- 1. 命名规范
- 1.1 通用命名规则
- 1.2 文件名字
- 1.3 类型名字
- 1.4 变量名字
- 1.5 常量名字
- 1.6 函数名字
- 1.7 命名空间
- 1.8 枚举名字
- 1.9 宏名称
- 1.10 命名规则的例外情况
- 2. 注释
- 2.1 注释风格
- 2.2 文件注释
- 2.3 结构体和类注释
- 2.4 类注释
- 2.5 函数注释
- 2.6 变量注释
- 2.7 函数参数注释
- 2.8 非建议行为
- 2.9 预期目标
- 2.10 代办事项
- 3. 代码格式化
- 3.1 行长度
- 3.2 非ASCII字符
- 3.3 空格和缩进(Space vs Tab)
- 3.4 函数声明和定义
- 3.5 lambda表达式
- 3.6 浮点数
- 3.7 函数调用
- 3.8 括号初始化
- 3.9 循环语句
- 3.10 指针和引用表达式
- 3.11 布尔表达式
- 3.12 返回值
- 3.13 变量和数组初始化
- 3.14 预处理器指令
- 3.15 类声明和定义
- 3.16 构造初始化列表
- 3.17 命名空间格式
- 3.18 水平空格
- 3.19 循环和条件语句
- 3.20 操作符
- 3.21 模板和强制类型转换
- 3.22 竖直方向空格
1. 命名规范
最重要的一致性规则是那些控制命名的规则。名称的风格会立即告诉我们命名实体是什么类型:类型、变量、函数、常量、宏等,而无需我们搜索该实体的声明。我们大脑中的模式匹配引擎很大程度上依赖于这些命名规则。
命名规则相当随意,但我们认为一致性比个人偏好更重要,因此无论您是否认为它们合理,规则就是规则(遵循一致性)。
1.1 通用命名规则
使用即使对不同团队的人来说也清晰的名称来优化可读性。
使用描述对象目的或意图的名称。不要担心节省水平方向的代码行长度,因为让新读者立即理解您的代码更为重要。
- 尽量减少使用项目外部人员可能不知道的缩写(尤其是首字母缩略词和首字母缩写词)。
- 不要通过删除单词中的字母来缩写。
- 根据经验,如果缩写在 Wikipedia 中列出,则可能没问题。
一般来说,描述性应与名称的可见范围成正比。例如,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()
(将RPC看成单个单词,而不是全大写的缩写词)。
1.2 文件名字
文件名应全部小写,可以包含下划线(_
) 或破折号 (-
)。请遵循项目使用的惯例。如果没有一致的本地模式可遵循,则最好使用“_
”。
可接受的文件名示例:
my_useful_class.cc
my-useful-class.cc
myusefulclass.cc
myusefulclass_test.cc // _unittest 和 _regtest 已弃用。
C++ 文件应具有 .cc
文件扩展名,头文件应具有 .h
扩展名。依赖于在特定点进行文本包含的文件应以 .inc
结尾(另请参阅自包含头文件部分)。
不要使用 /usr/include
中已存在的文件名,例如 db.h。
通常,请使文件名非常具体。
例如,使用 http_server_logs.h 而不是 logs.h。一种非常常见的情况是,有一对文件,例如 foo_bar.h 和 foo_bar.cc,定义一个名为 FooBar 的类。
1.3 类型名字
类型名称以大写字母开头,每个新单词都大写,没有下划线:MyExcitingClass、MyExcitingEnum。
所有类型的名称(类、结构、类型别名、枚举和类型模板参数)都具有相同的命名约定。类型名称应以大写字母开头,每个新单词都大写,没有下划线。例如:
// 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 class UrlTableError { ...
概念名称(Concept Names)遵循与类型名称相同的规则。
1.4 变量名字
变量(包括函数参数)和数据成员的名称采用蛇形命名法(snake_case)(全部小写,单词之间用下划线分隔)。类(但不是结构)的数据成员后面还有下划线。例如:a_local_variable、a_struct_data_member、a_class_data_member_。
常见变量名称:
std::string table_name; // OK - snake_case.
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;
};
1.5 常量名字
声明为 constexpr 或 const 的变量,其值在程序运行期间固定不变,以“k”开头,后面跟着大小写混合。在极少数情况下,当不能使用大写字母分隔时,可以使用下划线作为分隔符。例如:
const int kDaysInAWeek = 7;
const int kAndroid8_0_0 = 24; // Android 8.0.0
所有具有静态存储持续时间的变量(即静态变量和全局变量)都应以这种方式命名,包括模板中的变量,其中模板的不同实例可能具有不同的值。对于其他存储类别的变量(例如自动变量),此约定是可选的,否则适用通常的变量命名规则。例如:
void ComputeFoo(absl::string_view suffix) {
// Either of these is acceptable.
const absl::string_view kPrefix = "prefix";
const absl::string_view prefix = "prefix";
...
}
void ComputeFoo(absl::string_view suffix) {
// Bad - different invocations of ComputeFoo give kCombined different values.
const std::string kCombined = absl::StrCat(kPrefix, suffix);
...
}
1.6 函数名字
常规函数有大小写混合,访问器和修改器(Accessors and mutators)可以像变量一样命名。通常,函数应以大写字母开头,并且每个新单词都应大写。
AddTableEntry()
DeleteUrl()
OpenFileOrDie()
相同的命名规则适用于作为 API 的一部分公开的类和命名空间范围常量,这些常量旨在看起来像函数,因为它们是对象而不是函数,这一事实是一个不重要的实现细节。
访问器(Accessors)和变量(mutators)(获取和设置函数)可以像变量一样命名。它们通常对应于实际的成员变量,但这不是必需的。例如,int count()
和 void set_count(int count)
。
1.7 命名空间
命名空间名称全部小写,单词之间用下划线分隔。*命名空间名称基于项目名称。避免嵌套命名空间与众所周知的*命名空间之间发生冲突。
*命名空间的名称通常应为代码包含在该命名空间中的项目或团队的名称。该命名空间中的代码通常应位于基本名称与命名空间名称匹配的目录中(或其子目录中)。
请记住,禁止缩写名称的规则适用于命名空间,也适用于变量名称。命名空间内的代码很少需要提及命名空间名称,因此通常不需要缩写。
避免与众所周知的*命名空间匹配的嵌套命名空间。命名空间名称之间的冲突可能导致意外的构建中断,因为名称查找规则。特别是,不要创建任何嵌套的 std 命名空间。首选唯一的项目标识符(websearch::index
、websearch::index_util
),而不是容易发生冲突的名称,如 websearch::util
,还要避免过度嵌套命名空间。
对于内部命名空间,请警惕其他代码被添加到同一个内部命名空间,从而导致冲突(团队内的内部帮助程序往往相关,可能会导致冲突)。在这种情况下,使用文件名来创建唯一的内部名称会很有帮助(websearch::index::frobber_internal 用于 frobber.h)。
1.8 枚举名字
枚举器(适用于有作用域和无作用域枚举)应像常量一样命名,而不是像宏一样命名。也就是说,使用 kEnumName 而不是 ENUM_NAME。
enum class UrlTableError {
kOk = 0,
kOutOfMemory,
kMalformedInput,
};
// Not recommended
enum class AlternateUrlTableError {
OK = 0,
OUT_OF_MEMORY = 1,
MALFORMED_INPUT = 2,
};
在过去,枚举值的命名风格都像宏一样。这导致枚举值和宏之间出现名称冲突的问题。因此,我们进行了更改,倾向于使用常量样式命名。新代码应使用常量样式命名。
1.9 宏名称
您不会真的要定义宏吧?如果要定义,则宏如下所示:MY_MACRO_THAT_SCARES_SMALL_CHILDREN_AND_ADULTS_ALIKE。
一般情况下不应使用宏。但是,如果绝对需要宏,则应使用大写字母和下划线命名宏,并使用项目特定的前缀。
#define MYPROJECT_ROUND(x) ...
1.10 命名规则的例外情况
如果您要命名与现有 C 或 C++ 实体类似的内容,则可以遵循现有的命名约定方案。
bigopen() // 函数名称,遵循 open() 的形式
uint // typedef
bigpos // 结构或类,遵循 pos 的形式
sparse_hash_map // 类 STL 实体;遵循 STL 命名约定
LONGLONG_MAX // 常量,如 INT_MAX
2. 注释
注释对于保持代码的可读性至关重要。以下规则描述了您应该注释什么以及在哪里注释。但请记住:虽然注释非常重要,但最好的代码是自文档化的。为类型和变量赋予合理的名称比使用晦涩的名称(然后必须通过注释进行解释)要好得多。
2.1 注释风格
使用 //
或 /* */
语法,只要保持一致即可。
您可以使用 //
或 /* */
语法;不过,//
更为常见。注释方式和使用风格应保持一致。
2.2 文件注释
每个文件都以许可证样板文件开头。
如果源文件(例如 .h 文件)声明了多个面向用户的抽象(通用函数、相关类等),请包含描述这些抽象集合的注释。包含足够的细节,以便未来的作者知道哪些内容不适合。但是,关于各个抽象的详细文档属于这些抽象,而不是文件级别。
例如,如果您为 frobber.h
编写了文件注释,则无需在 frobber.cc
或 frobber_test.cc
中包含文件注释。
另一方面,如果您在 registered_objects.cc
中编写了没有关联头文件的类集合,则必须在 registered_objects.cc
中包含文件注释。
每个文件都应包含许可证样板。选择适合项目所用许可证的样板(例如,Apache 2.0、BSD、LGPL、GPL)。
如果您对带有作者行的文件进行了重大更改,请考虑删除作者行。新文件通常不应包含版权声明或作者行。
2.3 结构体和类注释
每个不明显的类或结构声明都应该有一个附带的注释,描述它的用途和如何使用它。
// Iterates over the contents of a GargantuanTable.
// Example:
// std::unique_ptr<GargantuanTableIterator> iter = table->NewIterator();
// for (iter->Seek("foo"); !iter->done(); iter->Next()) {
// process(iter->key(), iter->value());
// }
class GargantuanTableIterator {
...
};
2.4 类注释
类注释应为读者提供足够的信息,以了解如何以及何时使用类,以及正确使用类所需的任何其他注意事项。记录类所做的同步假设(如果有)。如果类的实例可以由多个线程访问,请特别注意记录围绕多线程使用的规则和不变量。
类注释通常是放置小示例代码片段的好地方,该代码片段演示了类的简单而集中的使用。
当足够分开时(例如,.h 和 .cc 文件),描述类使用的注释应与其接口定义一起使用;关于类操作和实现的注释应伴随类方法的实现。
2.5 函数注释
声明注释描述了函数的使用(当它不明显时),函数定义处的注释描述了操作。
几乎每个函数声明都应在其前面立即添加注释,以描述函数的功能及其使用方法。只有当函数简单且显而易见时(例如,类的明显属性的简单访问器),才可以省略这些注释。.cc 文件中声明的私有方法和函数不例外。函数注释应以此函数为隐含主语,并以动词短语开头;例如,Opens the file
,而不是Open the file
。通常,这些注释不会描述函数如何执行其任务。相反,这应该留给函数定义中的注释。
在函数声明的注释中要提及的内容类型:
-
输入和输出是什么:如果函数参数名称以反引号(
backticks
)提供,则代码索引工具可能能够更好地呈现文档。 - 对于类成员函数:对象是否在方法调用持续时间之外记住引用或指针参数,这对于构造函数的指针/引用参数来说很常见。
- 对于每个指针参数:是否允许它为空以及如果为空会发生什么。
- 对于每个输出或输入/输出参数:该参数所处的任何状态会发生什么?例如,状态是附加到还是覆盖?。
- 函数的使用方式是否对性能有任何影响。
下面是一个实际的例子:
// Returns an iterator for this table, positioned at the first entry
// lexically greater than or equal to `start_word`. If there is no
// such entry, returns a null pointer. The client must not use the
// iterator after the underlying GargantuanTable has been destroyed.
//
// This method is equivalent to:
// std::unique_ptr<Iterator> iter = table->NewIterator();
// iter->Seek(start_word);
// return iter;
std::unique_ptr<Iterator> GetIterator(absl::string_view start_word) const;
但是,不要过于冗长或陈述显而易见的内容。在记录函数重写时,请关注重写本身的细节,而不是重复重写函数的注释。在许多情况下,重写不需要额外的文档,因此不需要注释。
在注释构造函数和析构函数时,请记住,阅读代码的人知道构造函数和析构函数的用途,因此仅说“销毁此对象”之类的注释是没有用的。
记录构造函数对其参数执行的操作(例如,如果它们取得指针的所有权),以及析构函数执行的清理操作。如果这很简单,请跳过注释。析构函数没有标题注释是很常见的。
如果函数的执行方式存在任何棘手之处,则函数定义应该有解释性注释。例如,在定义注释中,您可以描述您使用的任何编码技巧,概述您经历的步骤,或解释为什么您选择以这种方式实现函数而不是使用可行的替代方案。例如,您可能会提到为什么它必须在函数的前半部分获取锁,但为什么后半部分不需要。
请注意,您不应该只是重复函数声明中给出的注释,无论是在 .h 文件还是其他地方。可以简要概括函数的作用,但注释的重点应该放在它是如何工作的。在您的实现中,您应该对代码中棘手的、不明显的、有趣的或重要的部分添加注释。
棘手或复杂的代码块应该在它们前面加上注释。
2.6 变量注释
一般来说,变量的实际名称应该足够描述性,以便清楚地说明变量的用途。在某些情况下,需要更多注释。
类数据成员:每个类数据成员(也称为实例变量或成员变量)的用途必须明确。如果类型和名称没有明确表达任何不变量(特殊值、成员之间的关系、生存期要求),则必须对其进行注释。但是,如果类型和名称足够(int num_events_;
),则无需注释。
特别是,当标记值(如 nullptr
或 -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 test cases that we run through in this regression test.
const int kNumTestCases = 6;
2.7 函数参数注释
当函数参数的含义不明显时,请考虑以下补救措施之一:
- 如果参数是文字常量,并且同一个常量在多个函数调用中使用,并且默认它们相同,则应使用命名常量来明确该约束,并保证它成立。
- 考虑更改函数签名,用枚举参数替换 bool 参数。这将使参数值具有自描述性。
- 对于具有多个配置选项的函数,请考虑定义一个类或结构来保存所有选项,并传递该选项的一个实例。这种方法有几个优点。选项在调用站点按名称引用,这澄清了它们的含义。它还减少了函数参数数量,使函数调用更易于读写。作为额外的好处,您在添加另一个选项时不必更改调用站点。
- 用命名变量替换大型或复杂的嵌套表达式。
- 作为最后的手段,使用注释来澄清调用站点的参数含义。
请考虑以下示例:
// 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);
2.8 非建议行为
不要陈述显而易见的事情。特别是,不要逐字描述代码的作用,除非行为对于精通 C++ 的读者来说并不明显。相反,提供更高级别的注释来描述代码为何这样做,或者使代码具有自我描述性。
// Find the element in the vector. <-- Bad: obvious!
if (std::find(v.begin(), v.end(), element) != v.end()) {
Process(element);
}
// Process "element" unless it was already processed.
if (std::find(v.begin(), v.end(), element) != v.end()) {
Process(element);
}
自描述代码不需要注释。上例中的注释显而易见:
if (!IsAlreadyProcessed(element)) {
Process(element);
}
2.9 预期目标
注意标点符号、拼写和语法,写得好的注释比写得不好的注释更容易阅读。
注释应与叙述性文本一样易读,并具有适当的大小写和标点符号。在许多情况下,完整的句子比句子片段更易读。较短的注释(例如代码行末尾的注释)有时可能不太正式,但您应该保持自己的风格。
虽然代码审阅者指出您在应该使用分号时使用了逗号可能会令人沮丧,但源代码保持高水平的清晰度和可读性非常重要。正确的标点符号、拼写和语法有助于实现这一目标。
2.10 代办事项
对于临时代码、短期解决方案或足够好但不完美的代码,请使用 TODO 注释。
TODO 应包含大写的字符串 TODO,后跟错误 ID、姓名、电子邮件地址或其他人员或问题标识符,并提供与 TODO 引用的问题相关的最佳上下文。
// TODO: bug 12345678 - Remove this after the 2047q4 compatibility window expires.
// TODO: example.com/my-design-doc - Manually fix up this code the next time it's touched.
// TODO(bug 12345678): Update this list after the Foo service is turned down.
// TODO(John): Use a "\*" here for concatenation operator.
如果您的 TODO 形式为At a future date do something
,请确保您包含一个非常具体的日期(Fix by November 2005
)或一个非常具体的事件(Remove this code when all clients can handle XML responses
)。
3. 代码格式化
编码风格和格式非常随意,但如果每个人都使用相同的风格,项目就会更容易遵循。个人可能不会同意格式规则的每个方面,有些规则可能需要一些时间来适应,但重要的是所有项目贡献者都遵循样式规则,以便他们都能轻松阅读和理解每个人的代码。
3.1 行长度
代码中每行文本的长度最多为 80 个字符。我们认识到这条规则是有争议的,但现有的很多代码已经遵守了这条规则,我们认为一致性很重要。
支持这条规则的人认为,强迫他们调整窗口大小是不礼貌的,而且没有必要再这样做了。有些人习惯于并排放置多个代码窗口,因此无论如何都没有空间来加宽窗口。人们在设置工作环境时假设了特定的最大窗口宽度,而 80 列一直是传统标准。为什么要改变它呢?
支持变革的人认为,更宽的行可以使代码更易读。80 列的限制是 20 世纪 60 年代大型机的陈旧做法;现代设备拥有宽屏幕,可以轻松显示更长的行。
建议一行最多 80 个字符。但在下述情况下,一行可以超过80个字符:
- 注释行无法拆分,否则会影响可读性、剪切粘贴或自动链接的便利性。例如,如果一行包含示例命令或文字 URL,长度超过 80 个字符。
- 字符串文字无法轻易换行到 80 列。这可能是因为它包含 URI 或其他语义关键部分,或者因为文字包含嵌入语言,或者多行文字的换行符很重要,如帮助消息。在这些情况下,拆分文字会降低可读性、可搜索性、点击链接的能力等。除了测试代码外,此类文字应出现在文件顶部附近的命名空间范围内。如果 Clang-Format 等工具无法识别不可拆分的内容,请根据需要禁用内容周围的工具。(我们必须在这些文字的可用性/可搜索性和它们周围代码的可读性之间取得平衡)。
- 包含语句
- 标头保护
- 使用声明
3.2 非ASCII字符
非 ASCII 字符应很少见,并且必须使用 UTF-8 格式。
您不应在源代码中对面向用户的文本进行硬编码,即使是英语,因此应尽量避免使用非 ASCII 字符。但是,在某些情况下,在代码中包含此类单词是合适的。例如,如果您的代码解析来自外部来源的数据文件,则可能适合将这些数据文件中使用的非 ASCII 字符串硬编码为分隔符。更常见的是,unittest 代码(不需要本地化)可能包含非 ASCII 字符串。在这种情况下,您应该使用 UTF-8,因为大多数工具都能理解这种编码,并且能够处理不仅仅是 ASCII 的编码。
十六进制编码也可以,并且鼓励在增强可读性的情况下使用十六进制编码 — 例如,“\xEF\xBB\xBF”,或者更简单的“\uFEFF”,是 Unicode 零宽度无间断空格字符,如果以直接 UTF-8 包含在源代码中,它将不可见。
尽可能避免使用 u8 前缀。从 C++20 开始,它的语义与 C++17 中的语义有显著不同,生成的是 char8_t 数组,而不是 char 数组,并且将在 C++23 中再次发生变化。
您不应使用 char16_t 和 char32_t 字符类型,因为它们适用于非 UTF-8 文本。出于类似的原因,您也不应该使用 wchar_t(除非您正在编写与 Windows API 交互的代码,而 Windows API 广泛使用 wchar_t)。
3.3 空格和缩进(Space vs Tab)
仅使用空格,并且每次缩进 2 个空格。
我们使用空格进行缩进。请勿在代码中使用制表符。您应该将编辑器设置为在按下制表符键时发出空格。
3.4 函数声明和定义
返回类型与函数名称在同一行,参数在同一行(如果合适)。将无法放在一行的参数列表换行,就像在函数调用中换行参数一样。
函数如下所示:
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
...
}
需要注意的几点:
-
选择好的参数名称。
-
仅当函数定义中未使用该参数时,才可以省略参数名称。
-
如果无法将返回类型和函数名称放在一行中,请在它们之间换行。
-
如果在函数声明或定义的返回类型后换行,请不要缩进。
-
左括号始终与函数名称在同一行。
-
函数名称和左括号之间永远没有空格。
-
括号和参数之间永远没有空格。
-
左花括号始终位于函数声明最后一行的末尾,而不是下一行的开头。
-
右花括号要么位于最后一行,要么与左花括号在同一行。
-
右括号和左花括号之间应该有一个空格。
-
如果可能,所有参数都应对齐。
-
默认缩进为 2 个空格。
-
包装的参数有 4 个空格的缩进。
从上下文中可以明显看出未使用的参数可以省略:
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_ATTRIBUTE_NOINLINE void ExpensiveFunction();
[[nodiscard]] bool IsOk();
3.5 lambda表达式
像其他函数一样格式化参数和函数体,并像其他逗号分隔列表一样捕获列表。
对于按引用捕获,请勿在与号 (&) 和变量名称之间留空格。
int x = 0;
auto x_plus_n = [&x](int n) -> int { return x + n; }
短 lambda 表达式可以作为函数参数内联编写。
absl::flat_hash_set<int> to_remove = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&to_remove](int i) {
return to_remove.contains(i);
}),
digits.end());
3.6 浮点数
浮点文字应始终带有小数点,且两边都有数字,即使使用指数表示法也是如此。如果所有浮点文字都采用这种熟悉的形式,可读性会得到提高,因为这有助于确保它们不会被误认为是整数文字,并且指数表示法的 E/e
不会被误认为是十六进制数字。
使用整数文字初始化浮点变量是可以的(假设变量类型可以精确表示该整数),但请注意,指数表示法中的数字永远不会是整数文字。
// 错误示例
float f = 1.f;
long double ld = -.5L;
double d = 1248e6;
// 正确示例
float f = 1.0f;
float f2 = 1.0; // Also OK
float f3 = 1; // Also OK
long double ld = -0.5L;
double d = 1248.0e6;
3.7 函数调用
要么将调用全部写在一行上,将参数括在括号中,要么在新行上开始参数,缩进四个空格,然后继续缩进四个空格。在没有其他考虑的情况下,使用最少的行数,包括在适当的地方在每行上放置多个参数。
函数调用具有以下格式:
bool result = DoSomething(argument1, argument2, argument3);
如果参数不能全部放在一行中,则应将其分成多行,每个后续行与第一个参数对齐。请勿在左括号后或右括号前添加空格:
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);
3.8 括号初始化
格式化带括号的初始化列表,就像格式化函数调用一样。如果带括号的列表跟在名称后面(例如类型或变量名称),则像具有该名称的函数调用一样初始化。如果没有名称,则假定名称长度为零。
// 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}};
3.9 循环语句
从高层次来看,循环或分支语句由以下部分组成:
- 一个或多个语句关键字(例如 if、else、switch、while、do 或 for)。
- 一个条件或迭代说明符(位于括号内)。
- 一个或多个受控语句或受控语句块。
对于这些语句:
- 语句的组成部分应由单个空格(而不是换行符)分隔。
- 在条件或迭代说明符中,在每个分号和下一个标记之间放置一个空格(或换行符),除非该标记是右括号或另一个分号。
- 在条件或迭代说明符中,不要在左括号后或右括号前放置空格。
- 将任何受控语句放在块内(即使用花括号)。
- 在受控块内,在左括号后立即放置一个换行符,在右括号前立即放置一个换行符。
if (condition) { // Good - no spaces inside parentheses, space before brace.
DoOneThing(); // Good - two-space indent.
DoAnotherThing();
} else if (int a = f(); a != 3) { // Good - closing brace on new line, else on same line.
DoAThirdThing(a);
} else {
DoNothing();
}
// Good - the same rules apply to loops.
while (condition) {
RepeatAThing();
}
// Good - the same rules apply to loops.
do {
RepeatAThing();
} while (condition);
// Good - the same rules apply to loops.
for (int i = 0; i < 10; ++i) {
RepeatAThing();
}
if(condition) {} // Bad - space missing after `if`.
else if ( condition ) {} // Bad - space between the parentheses and the condition.
else if (condition){} // Bad - space missing before `{`.
else if(condition){} // Bad - multiple spaces missing.
for (int a = f();a == 10) {} // Bad - space missing after the semicolon.
// Bad - `if ... else` statement does not have braces everywhere.
if (condition)
foo;
else {
bar;
}
// Bad - `if` statement too long to omit braces.
if (condition)
// Comment
DoSomething();
// Bad - `if` statement too long to omit braces.
if (condition1 &&
condition2)
DoSomething();
由于历史原因,我们允许上述规则有一个例外:如果整个语句出现在一行上(在这种情况下,右括号和受控语句之间有一个空格)或两行上(在这种情况下,右括号后有一个换行符并且没有括号),则可以省略受控语句的花括号或花括号内的换行符。
// OK - fits on one line.
if (x == kFoo) { return new Foo(); }
// OK - braces are optional in this case.
if (x == kFoo) return new Foo();
// OK - condition fits on one line, body fits on another.
if (x == kBar)
Bar(arg1, arg2, arg3);
此例外不适用于多关键字语句(例如 if … else 或 do … while)。
// Bad - `if ... else` statement is missing braces.
if (x) DoThis();
else DoThat();
// Bad - `do ... while` statement is missing braces.
do DoThis();
while (x);
仅在语句简短时使用此样式,并考虑使用花括号来使具有复杂条件或受控语句的循环和分支语句更具可读性。有些项目始终需要花括号。switch 语句中的 case 块可以有花括号,也可以没有,具体取决于您的偏好。如果您确实包含花括号,则应按如下所示放置它们。
switch (var) {
case 0: { // 2 space indent
Foo(); // 4 space indent
break;
}
default: {
Bar();
}
}
空循环体应该使用一对空的括号或者不用括号继续,而不是使用单个分号。
while (condition) {} // Good - `{}` indicates no logic.
while (condition) {
// Comments are okay, too
}
while (condition) continue; // Good - `continue` indicates no logic.
while (condition); // Bad - looks like part of `do-while` loop.
3.10 指针和引用表达式
句点或箭头周围没有空格。指针运算符没有尾随空格。以下是格式正确的指针和引用表达式的示例:
x = *p;
p = &x;
x = r.y;
x = r->y;
请注意:
-
访问成员时,句点或箭头周围没有空格。
-
指针运算符在
*
或&
后没有空格。
引用指针或引用(变量声明或定义、参数、返回类型、模板参数等)时,您可以在星号/与号之前或之后放置空格。在尾随空格样式中,在某些情况下会省略空格(模板参数等)。
// These are fine, space preceding.
char *c;
const std::string &str;
int *GetPointer();
std::vector<char *>
// These are fine, space following (or elided).
char* c;
const std::string& str;
int* GetPointer();
std::vector<char*> // Note no space between '*' and '>'
您应该在单个文件中始终如一地执行此操作。修改现有文件时,请使用该文件中的样式。
允许(如果不常见)在同一个声明中声明多个变量,但如果其中任何一个变量具有指针或引用修饰,则不允许这样做。此类声明很容易被误读。
// Fine if helpful for readability.
int x, y;
int x, *y; // Disallowed - no & or * in multiple declaration
int* x, *y; // Disallowed - no & or * in multiple declaration; inconsistent spacing
char * c; // Bad - spaces on both sides of *
const std::string & str; // Bad - spaces on both sides of &
3.11 布尔表达式
如果布尔表达式的长度超过标准行长,则应在分行方式上保持一致。
在此示例中,逻辑 AND 运算符始终位于行尾:
if (this_one_thing > this_other_thing &&
a_third_thing == a_fourth_thing &&
yet_another && last_one) {
...
}
请注意,本例中的代码换行时,两个 &&
逻辑且运算符都位于行尾。这在 Google 代码中更为常见,不过也允许将所有运算符换行到行首。您可以随意插入额外的括号,因为如果使用得当,它们可以非常有助于提高可读性,但要小心不要过度使用。还请注意,您应该始终使用标点符号运算符(例如 && 和 ~),而不是单词运算符(例如 and
和 compl
)。
3.12 返回值
不要不必要地用括号将返回表达式括起来。仅在 x = expr;
中使用括号的情况下,才在 return 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!
3.13 变量和数组初始化
您可以在 =、() 和 {} 之间进行选择;以下都是正确的:
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
构造函数的类型上使用带括号的初始化列表 {...}
时要小心。非空带括号的初始化列表会尽可能优先使用 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.
3.14 预处理器指令
启动预处理器指令的#
号应始终位于行首。
即使预处理器指令位于缩进代码主体内,指令也应从行首开始。
// 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();
}
3.15 类声明和定义
按公共、受保护和私有顺序排列的部分,每个部分缩进一个空格。
类定义的基本格式(缺少注释,请参阅类注释以了解需要哪些注释)是:
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_;
};
注意事项:
- 任何基类名称都应与子类名称位于同一行,但不得超过 80 列。
-
public:
、protected:
和private:
关键字应缩进一个空格。 - 除第一个实例外,这些关键字前面应有一个空行。此规则在小类中是可选的。
- 这些关键字后不要留空行。
-
public
部分应放在最前面,然后是protected
部分,最后是private
部分。 - 有关在每个部分中对声明进行排序的规则,请参阅声明顺序。
3.16 构造初始化列表
构造函数初始化列表可以全部放在一行上,也可以在后续行中缩进四个空格。
初始化列表可接受的格式为:
// 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) {}
3.17 命名空间格式
命名空间的内容不缩进。命名空间不会添加额外的缩进级别。例如,使用:
namespace {
void foo() { // Correct. No extra indentation within namespace.
...
}
} // namespace
不要在命名空间内缩进:
namespace {
// Wrong! Indented when it should not be.
void foo() {
...
}
} // namespace
3.18 水平空格
水平空格的使用取决于位置。切勿将尾随空格放在行末。
int i = 0; // Two spaces before end-of-line comments.
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.
...
添加尾随空格会导致编辑同一文件的其他人在合并时需要做额外的工作,删除现有的尾随空格也会导致这种情况。因此:不要引入尾随空格。如果您已经更改了该行,请将其删除,或者在单独的清理操作中执行此操作(最好在没有其他人处理该文件时)。
3.19 循环和条件语句
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.
3.20 操作符
// 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)
...
3.21 模板和强制类型转换
// 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;
3.22 竖直方向空格
尽量减少使用垂直空格。
这更像是一个原则,而不是规则:如果没有必要,请不要使用空行。特别是,不要在函数之间放置超过一两个空行,不要用空行开始函数,不要用空行结束函数,并且要谨慎使用空行。代码块中的空行就像散文中的段落分隔符:在视觉上分隔两个想法。
基本原则是:一个屏幕上显示的代码越多,就越容易跟踪和理解程序的控制流。有目的地使用空格来分隔该流程。
一些经验法则可以帮助提高空行的实用性:
- 函数开头或结尾的空行无助于提高可读性。
- if-else 块链中的空行可能有助于提高可读性。
- 注释行之前的空行通常有助于提高可读性——新注释的引入暗示了新想法的开始,而空行清楚地表明注释与以下内容有关,而不是与前面的内容有关。
- 命名空间或命名空间块声明中的空白行可通过在视觉上将承载内容与(很大程度上非语义的)组织包装器分开来提高可读性。尤其是当命名空间中的第一个声明前面有注释时,这将成为上一条规则的特殊情况,有助于注释“附加”到后续声明。