C++语言编程规范
目的
规则并不是完美的,通过禁止在特定情况下有用的特性,可能会对代码实现造成影响。但是我们制定规则的目的“为了大多数程序员可以得到更多的好处”, 如果在团队运作中认为某个规则无法遵循,希望可以共同改进该规则。
参考该规范之前,希望您具有相应的C++语言基础能力,而不是通过该文档来学习C++语言。
- 了解C++语言的ISO标准;
- 熟知C++语言的基本语言特性,包括C++ 03/11/14/17相关特性;
- 了解C++语言的标准库;
总体原则
代码需要在保证功能正确的前提下,满足可读、可维护、安全、可靠、可测试、高效、可移植的特征要求。
重点关注
- 约定C++语言的编程风格,比如命名,排版等。
- C++语言的模块化设计,如何设计头文件,类,接口和函数。
- C++语言相关特性的优秀实践,比如常量,类型转换,资源管理,模板等。
- 现代C++语言的优秀实践,包括C++11/14/17中可以提高代码可维护性,提高代码可靠性的相关约定。
- 本规范优先适于用C++17版本。
约定
规则:编程时必须遵守的约定(must)
建议:编程时应该遵守的约定(should)
本规范适用通用C++标准, 如果没有特定的标准版本,适用所有的版本(C++03/11/14/17)。
例外
无论是'规则'还是'建议',都必须理解该条目这么规定的原因,并努力遵守。
但是,有些规则和建议可能会有例外。
在不违背总体原则,经过充分考虑,有充足的理由的前提下,可以适当违背规范中约定。
例外破坏了代码的一致性,请尽量避免。'规则'的例外应该是极少的。
下列情况,应风格一致性原则优先:
修改外部开源代码、第三方代码时,应该遵守开源代码、第三方代码已有规范,保持风格统一。
2 命名
通用命名
驼峰风格(CamelCase)
大小写字母混用,单词连在一起,不同单词间通过单词首字母大写来分开。
按连接后的首字母是否大写,又分: 大驼峰(UpperCamelCase)和小驼峰(lowerCamelCase)
类型 | 命名风格 |
类类型,结构体类型,枚举类型,联合体类型等类型定义, 作用域名称 | 大驼峰 |
函数(包括全局函数,作用域函数,成员函数) | 大驼峰 |
全局变量(包括全局和命名空间域下的变量,类静态变量),局部变量,函数参数,类、结构体和联合体中的成员变量 | 小驼峰 |
宏,常量(const),枚举值,goto 标签 | 全大写,下划线分割 |
注意:
上表中常量是指全局作用域、namespace域、类的静态成员域下,以 const或constexpr 修饰的基本数据类型、枚举、字符串类型的变量,不包括数组和其他类型变量。
上表中变量是指除常量定义以外的其他变量,均使用小驼峰风格。
文件命名
规则2.2.1 C++文件以.cpp结尾,头文件以.h结尾
我们推荐使用.h作为头文件的后缀,这样头文件可以直接兼容C和C++。
我们推荐使用.cpp作为实现文件的后缀,这样可以直接区分C++代码,而不是C代码。
目前业界还有一些其他的后缀的表示方法:
- 头文件: .hh, .hpp, .hxx
- cpp文件:.cc, .cxx, .c
如果当前项目组使用了某种特定的后缀,那么可以继续使用,但是请保持风格统一。
但是对于本文档,我们默认使用.h和.cpp作为后缀。
规则2.2.2 C++文件名和类名保持一致
C++的头文件和cpp文件名和类名保持一致,使用下划线小写风格。
如果有一个类叫DatabaseConnection,那么对应的文件名:
- database_connection.h
- database_connection.cpp
结构体,命名空间,枚举等定义的文件名类似。
函数命名
函数命名统一使用大驼峰风格,一般采用动词或者动宾结构。
class List {
public:
void AddElement(const Element& element);
Element GetElement(const unsigned int index) const;
bool IsEmpty() const;
};
namespace Utils {
void DeleteUser();
}
类型命名
类型命名采用大驼峰命名风格。
所有类型命名——类、结构体、联合体、类型定义(typedef)、枚举——使用相同约定,例如:
// classes, structs and unions
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
union Packet { ...
// typedefs
typedef std::map<std::string, UrlTableProperties*> PropertiesMap;
// enums
enum UrlTableErrors { ...
对于命名空间的命名,建议使用大驼峰:
// namespace
namespace OsUtils {
namespace FileUtils {
}
}
建议2.4.1 避免滥用 typedef或者#define 对基本类型起别名
除有明确的必要性,否则不要用 typedef/#define 对基本数据类型进行重定义。
优先使用<cstdint>
头文件中的基本类型:
有符号类型 | 无符号类型 | 描述 |
int8_t | uint8_t | 宽度恰为8的有/无符号整数类型 |
int16_t | uint16_t | 宽度恰为16的有/无符号整数类型 |
int32_t | uint32_t | 宽度恰为32的有/无符号整数类型 |
int64_t | uint64_t | 宽度恰为64的有/无符号整数类型 |
intptr_t | uintptr_t | 足以保存指针的有/无符号整数类型 |
变量命名
通用变量命名采用小驼峰,包括全局变量,函数形参,局部变量,成员变量。
std::string tableName; // Good: 推荐此风格
std::string tablename; // Bad: 禁止此风格
std::string path; // Good: 只有一个单词时,小驼峰为全小写
规则2.5.1 全局变量应增加 'g_' 前缀,静态变量命名不需要加特殊前缀
全局变量是应当尽量少使用的,使用时应特别注意,所以加上前缀用于视觉上的突出,促使开发人员对这些变量的使用更加小心。
- 全局静态变量命名与全局变量相同。
- 函数内的静态变量命名与普通局部变量相同。
- 类的静态成员变量和普通成员变量相同。
int g_activeConnectCount;
void Func()
{
static int packetCount = 0;
...
}
规则2.5.2 类的成员变量命名以小驼峰加后下划线组成
class Foo {
private:
std::string fileName_; // 添加_后缀,类似于K&R命名风格
};
对于struct/union的成员变量,仍采用小驼峰不加后缀的命名方式,与局部变量命名风格一致。
宏、常量、枚举命名
宏、枚举值采用全大写,下划线连接的格式。
全局作用域内,有名和匿名namespace内的 const 常量,类的静态成员常量,全大写,下划线连接;函数局部 const 常量和类的普通const成员变量,使用小驼峰命名风格。
#define MAX(a, b) (((a) < (b)) ? (b) : (a)) // 仅对宏命名举例,并不推荐用宏实现此类功能
enum TintColor { // 注意,枚举类型名用大驼峰,其下面的取值是全大写,下划线相连
RED,
DARK_RED,
GREEN,
LIGHT_GREEN
};
int Func(...)
{
const unsigned int bufferSize = 100; // 函数局部常量
char *p = new char[bufferSize];
...
}
namespace Utils {
const unsigned int DEFAULT_FILE_SIZE_KB = 200; // 全局常量
}
3 格式
行宽
规则3.1.1 行宽不超过 120 个字符
建议每行字符数不要超过 120 个。如果超过120个字符,请选择合理的方式进行换行。
例外:
缩进
规则3.2.1 使用空格进行缩进,每次缩进4个空格
只允许使用空格(space)进行缩进,每次缩进为 4 个空格。不允许使用Tab符进行缩进。
当前几乎所有的集成开发环境(IDE)都支持配置将Tab符自动扩展为4空格输入;请配置你的IDE支持使用空格进行缩进。
大括号
规则3.3.1 使用 K&R 缩进风格
K&R风格
换行时,函数(不包括lambda表达式)左大括号另起一行放行首,并独占一行;其他左大括号跟随语句放行末。
右大括号独占一行,除非后面跟着同一语句的剩余部分,如 do 语句中的 while,或者 if 语句的 else/else if,或者逗号、分号。
如:
struct MyType { // 跟随语句放行末,前置1空格
...
};
int Foo(int a)
{ // 函数左大括号独占一行,放行首
if (...) {
...
} else {
...
}
}
推荐这种风格的理由:
- 代码更紧凑;
- 相比另起一行,放行末使代码阅读节奏感上更连续;
- 符合后来语言的习惯,符合业界主流习惯;
- 现代集成开发环境(IDE)都具有代码缩进对齐显示的辅助功能,大括号放在行尾并不会对缩进和范围产生理解上的影响。
对于空函数体,可以将大括号放在同一行:
class MyClass {
public:
MyClass() : value_(0) {}
private:
int value_;
};
函数声明和定义
规则3.4.1 函数声明和定义的返回类型和函数名在同一行;函数参数列表超出行宽时要换行并合理对齐
在声明和定义函数的时候,函数的返回值类型应该和函数名在同一行;如果行宽度允许,函数参数也应该放在一行;否则,函数参数应该换行,并进行合理对齐。
参数列表的左圆括号总是和函数名在同一行,不要单独一行;右圆括号总是跟随最后一个参数。
换行举例:
ReturnType FunctionName(ArgType paramName1, ArgType paramName2) // Good:全在同一行
{
...
}
ReturnType VeryVeryVeryLongFunctionName(ArgType paramName1, // 行宽不满足所有参数,进行换行
ArgType paramName2, // Good:和上一行参数对齐
ArgType paramName3)
{
...
}
ReturnType LongFunctionName(ArgType paramName1, ArgType paramName2, // 行宽限制,进行换行
ArgType paramName3, ArgType paramName4, ArgType paramName5) // Good: 换行后 4 空格缩进
{
...
}
ReturnType ReallyReallyReallyReallyLongFunctionName( // 行宽不满足第1个参数,直接换行
ArgType paramName1, ArgType paramName2, ArgType paramName3) // Good: 换行后 4 空格缩进
{
...
}
函数调用
规则3.5.1 函数调用入参列表应放在一行,超出行宽换行时,保持参数进行合理对齐
函数调用时,函数参数列表放在一行。参数列表如果超过行宽,需要换行并进行合理的参数对齐。
左圆括号总是跟函数名,右圆括号总是跟最后一个参数。
换行举例:
ReturnType result = FunctionName(paramName1, paramName2); // Good:函数参数放在一行
ReturnType result = FunctionName(paramName1,
paramName2, // Good:保持与上方参数对齐
paramName3);
ReturnType result = FunctionName(paramName1, paramName2,
paramName3, paramName4, paramName5); // Good:参数换行,4 空格缩进
ReturnType result = VeryVeryVeryLongFunctionName( // 行宽不满足第1个参数,直接换行
paramName1, paramName2, paramName3); // 换行后,4 空格缩进
如果函数调用的参数存在内在关联性,按照可理解性优先于格式排版要求,对参数进行合理分组换行。
// Good:每行的参数代表一组相关性较强的数据结构,放在一行便于理解
int result = DealWithStructureLikeParams(left.x, left.y, // 表示一组相关参数
right.x, right.y); // 表示另外一组相关参数
if语句
规则3.6.1 if语句必须要使用大括号
我们要求if语句都需要使用大括号,即便只有一条语句。
理由:
- 代码逻辑直观,易读;
- 在已有条件语句代码上增加新代码时不容易出错;
- 对于在if语句中使用函数式宏时,有大括号保护不易出错(如果宏定义时遗漏了大括号)。
if (objectIsNotExist) { // Good:单行条件语句也加大括号
return CreateNewObject();
}
规则3.6.2 禁止 if/else/else if 写在同一行
条件语句中,若有多个分支,应该写在不同行。
如下是正确的写法:
if (someConditions) {
DoSomething();
...
} else { // Good: else 与 if 在不同行
...
}
下面是不符合规范的案例:
if (someConditions) { ... } else { ... } // Bad: else 与 if 在同一行
循环语句
规则3.7.1 循环语句必须使用大括号
和条件表达式类似,我们要求for/while循环语句必须加上大括号,即便循环体是空的,或循环语句只有一条。
for (int i = 0; i < someRange; i++) { // Good: 使用了大括号
DoSomething();
}
while (condition) { } // Good:循环体是空,使用大括号
while (condition) {
continue; // Good:continue 表示空逻辑,使用大括号
}
坏的例子:
for (int i = 0; i < someRange; i++)
DoSomething(); // Bad: 应该加上括号
while (condition); // Bad:使用分号容易让人误解是while语句中的一部分
switch语句
规则3.8.1 switch 语句的 case/default 要缩进一层
switch 语句的缩进风格如下:
switch (var) {
case 0: // Good: 缩进
DoSomething1(); // Good: 缩进
break;
case 1: { // Good: 带大括号格式
DoSomething2();
break;
}
default:
break;
}
switch (var) {
case 0: // Bad: case 未缩进
DoSomething();
break;
default: // Bad: default 未缩进
break;
}
表达式
建议3.9.1 表达式换行要保持换行的一致性,运算符放行末
较长的表达式,不满足行宽要求的时候,需要在适当的地方换行。一般在较低优先级运算符或连接符后面截断,运算符或连接符放在行末。
运算符、连接符放在行末,表示“未结束,后续还有”。
例:
// 假设下面第一行已经不满足行宽要求
if ((currentValue > threshold) && // Good:换行后,逻辑操作符放在行尾
someCondition) {
DoSomething();
...
}
int result = reallyReallyLongVariableName1 + // Good
reallyReallyLongVariableName2;
表达式换行后,注意保持合理对齐,或者4空格缩进。参考下面例子
int sum = longVariableName1 + longVariableName2 + longVariableName3 +
longVariableName4 + longVariableName5 + longVariableName6; // Good: 4空格缩进
int sum = longVariableName1 + longVariableName2 + longVariableName3 +
longVariableName4 + longVariableName5 + longVariableName6; // Good: 保持对齐
变量赋值
规则3.10.1 多个变量定义和赋值语句不允许写在一行
每行只有一个变量初始化的语句,更容易阅读和理解。
int maxCount = 10;
bool isCompleted = false;
下面是不符合规范的示例:
int maxCount = 10; bool isCompleted = false; // Bad:多个变量初始化需要分开放在多行,每行一个变量初始化
int x, y = 0; // Bad:多个变量定义需要分行,每行一个
int pointX;
int pointY;
...
pointX = 1; pointY = 2; // Bad:多个变量赋值语句放同一行
例外:for 循环头、if 初始化语句(C++17)、结构化绑定语句(C++17)中可以声明和初始化多个变量。这些语句中的多个变量声明有较强关联,如果强行分成多行会带来作用域不一致,声明和初始化割裂等问题。
初始化
初始化包括结构体、联合体、及数组的初始化
规则3.11.1 初始化换行时要有缩进,并进行合理对齐
结构体或数组初始化时,如果换行应保持4空格缩进。
从可读性角度出发,选择换行点和对齐位置。
const int rank[] = {
16, 16, 16, 16, 32, 32, 32, 32,
64, 64, 64, 64, 32, 32, 32, 32
};
指针与引用
建议3.12.1 指针类型"*
"跟随变量名或者类型,不要两边都留有或者都没有空格
指针命名: *
靠左靠右都可以,但是不要两边都有或者都没有空格。
int* p = nullptr; // Good
int *p = nullptr; // Good
int*p = nullptr; // Bad
int * p = nullptr; // Bad
例外:当变量被 const 修饰时,"*
" 无法跟随变量,此时也不要跟随类型。
const char * const VERSION = "V100";
建议3.12.2 引用类型"&
"跟随变量名或者类型,不要两边都留有或者都没有空格
引用命名:&
靠左靠右都可以,但是不要两边都有或者都没有空格。
int i = 8;
int& p = i; // Good
int &p = i; // Good
int*& rp = pi; // Good,指针的引用,*& 一起跟随类型
int *&rp = pi; // Good,指针的引用,*& 一起跟随变量名
int* &rp = pi; // Good,指针的引用,* 跟随类型,& 跟随变量名
int & p = i; // Bad
int&p = i; // Bad
编译预处理
规则3.13.1 编译预处理的"#"统一放在行首,嵌套编译预处理语句时,"#"可以进行缩进
编译预处理的"#"统一放在行首,即使编译预处理的代码是嵌入在函数体中的,"#"也应该放在行首。
规则3.13.2 避免使用宏
宏会忽略作用域,类型系统以及各种规则,容易引发问题。应尽量避免使用宏定义,如果必须使用宏,要保证证宏名的唯一性。
在C++中,有许多方式来避免使用宏:
- 用const或enum定义易于理解的常量
- 用namespace避免名字冲突
- 用inline函数避免函数调用的开销
- 用template函数来处理多种类型
在文件头保护宏、条件编译、日志记录等必要场景中可以使用宏。
规则3.13.3 禁止使用宏来表示常量
宏是简单的文本替换,在预处理阶段完成,运行报错时直接报相应的值;跟踪调试时也是显示值,而不是宏名; 宏没有类型检查,不安全; 宏没有作用域。
规则3.13.4 禁止使用函数式宏
宏义函数式宏前,应考虑能否用函数替代。对于可替代场景,建议用函数替代宏。
函数式宏的缺点如下:
- 函数式宏缺乏类型检查,不如函数调用检查严格
- 宏展开时宏参数不求值,可能会产生非预期结果
- 宏没有独立的作用域
- 宏的技巧性太强,例如#的用法和无处不在的括号,影响可读性
- 在特定场景中必须用编译器对宏的扩展语法,如GCC的statement expression,影响可移植性
- 宏在预编译阶段展开后,在期后编译、链接和调试时都不可见;而且包含多行的宏会展开为一行。函数式宏难以调试、难以打断点,不利于定位问题
- 对于包含大量语句的宏,在每个调用点都要展开。如果调用点很多,会造成代码空间的膨胀
函数没有宏的上述缺点。但是,函数相比宏,最大的劣势是执行效率不高(增加函数调用的开销和编译器优化的难度)。
为此,可以在必要时使用内联函数。内联函数跟宏类似,也是在调用点展开。不同之处在于内联函数是在编译时展开。
内联函数兼具函数和宏的优点:
- 内联函数执行严格的类型检查
- 内联函数的参数求值只会进行一次
- 内联函数就地展开,没有函数调用的开销
- 内联函数比函数优化得更好
对于性能要求高的产品代码,可以考虑用内联函数代替函数。
例外:
在日志记录场景中,需要通过函数式宏保持调用点的文件名(FILE)、行号(LINE)等信息。
空格和空行
规则3.14.1 水平空格应该突出关键字和重要信息,避免不必要的留白
水平空格应该突出关键字和重要信息,每行代码尾部不要加空格。总体规则如下:
- if, switch, case, do, while, for等关键字之后加空格;
- 小括号内部的两侧,不要加空格;
- 大括号内部两侧有无空格,左右必须保持一致;
- 一元操作符(& * + ‐ ~ !)之后不要加空格;
- 二元操作符(= + ‐ < > * / % | & ^ <= >= == != )左右两侧加空格
- 三目运算符(? :)符号两侧均需要空格
- 前置和后置的自增、自减(++ --)和变量之间不加空格
- 结构体成员操作符(. ->)前后不加空格
- 逗号(,)前面不加空格,后面增加空格
- 对于模板和类型转换(<>)和类型之间不要添加空格
- 域操作符(:🙂前后不要添加空格
- 冒号(🙂前后根据情况来判断是否要添加空格
常规情况:
void Foo(int b) { // Good:大括号前应该留空格
int i = 0; // Good:变量初始化时,=前后应该有空格,分号前面不要留空格
int buf[BUF_SIZE] = {0}; // Good:大括号内两侧都无空格
函数定义和函数调用:
int result = Foo(arg1,arg2);
^ // Bad: 逗号后面需要增加空格
int result = Foo( arg1, arg2 );
^ ^ // Bad: 函数参数列表的左括号后面不应该有空格,右括号前面不应该有空格
指针和取地址
x = *p; // Good:*操作符和指针p之间不加空格
p = &x; // Good:&操作符和变量x之间不加空格
x = r.y; // Good:通过.访问成员变量时不加空格
x = r->y; // Good:通过->访问成员变量时不加空格
操作符:
x = 0; // Good:赋值操作的=前后都要加空格
x = -5; // Good:负数的符号和数值之前不要加空格
++x; // Good:前置和后置的++/--和变量之间不要加空格
x--;
if (x && !y) // Good:布尔操作符前后要加上空格,!操作和变量之间不要空格
v = w * x + y / z; // Good:二元操作符前后要加空格
v = w * (x + z); // Good:括号内的表达式前后不需要加空格
int a = (x < y) ? x : y; // Good: 三目运算符, ?和:前后需要添加空格
循环和条件语句:
if (condition) { // Good:if关键字和括号之间加空格,括号内条件语句前后不加空格
...
} else { // Good:else关键字和大括号之间加空格
...
}
while (condition) {} // Good:while关键字和括号之间加空格,括号内条件语句前后不加空格
for (int i = 0; i < someRange; ++i) { // Good:for关键字和括号之间加空格,分号之后加空格
...
}
switch (condition) { // Good: switch 关键字后面有1空格
case 0: // Good:case语句条件和冒号之间不加空格
...
break;
...
default:
...
break;
}
模板和转换
// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有.
vector<string> x;
y = static_cast<char*>(x);
// 在类型与指针操作符之间留空格也可以, 但要保持一致.
vector<char *> x;
域操作符
std::cout; // Good: 命名空间访问,不要留空格
int MyClass::GetValue() const {} // Good: 对于成员函数定义,不要留空格
冒号
// 添加空格的场景
// Good: 类的派生需要留有空格
class Sub : public Base {
};
// 构造函数初始化列表需要留有空格
MyClass::MyClass(int var) : someVar_(var)
{
DoSomething();
}
// 位域表示也留有空格
struct XX {
char a : 4;
char b : 5;
char c : 4;
};
// 不添加空格的场景
// Good: 对于public:, private:这种类访问权限的冒号不用添加空格
class MyClass {
public:
MyClass(int var);
private:
int someVar_;
};
// 对于switch-case的case和default后面的冒号不用添加空格
switch (value)
{
case 1:
DoSomething();
break;
default:
break;
}
注意:当前的集成开发环境(IDE)可以设置删除行尾的空格,请正确配置。
建议3.14.1 合理安排空行,保持代码紧凑
减少不必要的空行,可以显示更多的代码,方便代码阅读。下面有一些建议遵守的规则:
- 根据上下内容的相关程度,合理安排空行;
- 函数内部、类型定义内部、宏内部、初始化表达式内部,不使用连续空行
- 不使用连续 3 个空行,或更多
- 大括号内的代码块行首之前和行尾之后不要加空行,但namespace的大括号内不作要求。
int Foo()
{
...
}
int Bar() // Bad:最多使用连续2个空行。
{
...
}
if (...) {
// Bad:大括号内的代码块行首不要加入空行
...
// Bad:大括号内的代码块行尾不要加入空行
}
int Foo(...)
{
// Bad:函数体内行首不要加空行
...
}
类
规则3.15.1 类访问控制块的声明依次序是 public:, protected:, private:,缩进和 class 关键字对齐
class MyClass : public BaseClass {
public: // 注意没有缩进
MyClass(); // 标准的4空格缩进
explicit MyClass(int var);
~MyClass() {}
void SomeFunction();
void SomeFunctionThatDoesNothing()
{
}
void SetVar(int var) { someVar_ = var; }
int GetVar() const { return someVar_; }
private:
bool SomeInternalFunction();
int someVar_;
int someOtherVar_;
};
在各个部分中,建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括 typedef, using 和嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它成员函数, 数据成员。
规则3.15.2 构造函数初始化列表放在同一行或按四格缩进并排多行
// 如果所有变量能放在同一行:
MyClass::MyClass(int var) : someVar_(var)
{
DoSomething();
}
// 如果不能放在同一行,
// 必须置于冒号后, 并缩进4个空格
MyClass::MyClass(int var)
: someVar_(var), someOtherVar_(var + 1) // Good: 逗号后面留有空格
{
DoSomething();
}
// 如果初始化列表需要置于多行, 需要逐行对齐
MyClass::MyClass(int var)
: someVar_(var), // 缩进4个空格
someOtherVar_(var + 1)
{
DoSomething();
}
4 注释
一般的,尽量通过清晰的架构逻辑,好的符号命名来提高代码可读性;需要的时候,才辅以注释说明。
注释是为了帮助阅读者快速读懂代码,所以要从读者的角度出发,按需注释。
注释内容要简洁、明了、无二义性,信息全面且不冗余。
注释跟代码一样重要。
写注释时要换位思考,用注释去表达此时读者真正需要的信息。在代码的功能、意图层次上进行注释,即注释解释代码难以表达的意图,不要重复代码信息。
修改代码时,也要保证其相关注释的一致性。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,让阅读者迷惑、费解,甚至误解。
使用英文进行注释。
注释风格
在 C++ 代码中,使用 /*
*/
和 //
都是可以的。
按注释的目的和位置,注释可分为不同的类型,如文件头注释、函数头注释、代码注释等等;
同一类型的注释应该保持统一的风格。
注意:本文示例代码中,大量使用 '//' 后置注释只是为了更精确的描述问题,并不代表这种注释风格更好。
文件头注释
规则3.1 文件头注释必须包含版权许可
/*
- Copyright (c) 2020 XXX
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
*
- http://www.apache.org/licenses/LICENSE-2.0
*
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
*/
函数头注释
规则4.3.1 公有(public)函数必须编写函数头注释
公有函数属于类对外提供的接口,调用者需要了解函数的功能、参数的取值范围、返回的结果、注意事项等信息才能正常使用。
特别是参数的取值范围、返回的结果、注意事项等都无法做到自注示,需要编写函数头注释辅助说明。
规则4.3.2 禁止空有格式的函数头注释
并不是所有的函数都需要函数头注释;
函数签名无法表达的信息,加函数头注释辅助说明;
函数头注释统一放在函数声明或定义上方,使用如下风格之一:
使用//
写函数头
// 单行函数头
int Func1(void);
// 多行函数头
// 第二行
int Func2(void);
使用/* */
写函数头
/* 单行函数头 */
int Func1(void);
/*
* 另一种单行函数头
*/
int Func2(void);
/*
* 多行函数头
* 第二行
*/
int Func3(void);
函数尽量通过函数名自注释,按需写函数头注释。
不要写无用、信息冗余的函数头;不要写空有格式的函数头。
函数头注释内容可选,但不限于:功能说明、返回值,性能约束、用法、内存约定、算法实现、可重入的要求等等。
模块对外头文件中的函数接口声明,其函数头注释,应当将重要、有用的信息表达清楚。
例:
/*
* 返回实际写入的字节数,-1表示写入失败
* 注意,内存 buf 由调用者负责释放
*/
int WriteString(const char *buf, int len);
坏的例子:
/*
* 函数名:WriteString
* 功能:写入字符串
* 参数:
* 返回值:
*/
int WriteString(const char *buf, int len);
上面例子中的问题:
- 参数、返回值,空有格式没内容
- 函数名信息冗余
- 关键的 buf 由谁释放没有说清楚
代码注释
规则4.4.1 代码注释放于对应代码的上方或右边
规则4.4.2 注释符与注释内容间要有1空格;右置注释与前面代码至少1空格
代码上方的注释,应该保持对应代码一样的缩进。
选择并统一使用如下风格之一:
使用//
// 这是单行注释
DoSomething();
// 这是多行注释
// 第二行
DoSomething();
使用/*' '*/
/* 这是单行注释 */
DoSomething();
/*
* 另一种方式的多行注释
* 第二行
*/
DoSomething();
代码右边的注释,与代码之间,至少留1空格,建议不超过4空格。
通常使用扩展后的 TAB 键即可实现 1-4 空格的缩进。
选择并统一使用如下风格之一:
int foo = 100; // 放右边的注释
int bar = 200; /* 放右边的注释 */
右置格式在适当的时候,上下对齐会更美观。
对齐后的注释,离左边代码最近的那一行,保证1-4空格的间隔。
例:
const int A_CONST = 100; /* 相关的同类注释,可以考虑上下对齐 */
const int ANOTHER_CONST = 200; /* 上下对齐时,与左侧代码保持间隔 */
当右置的注释超过行宽时,请考虑将注释置于代码上方。
规则4.4.3 不用的代码段直接删除,不要注释掉
被注释掉的代码,无法被正常维护;当企图恢复使用这段代码时,极有可能引入易被忽略的缺陷。
正确的做法是,不需要的代码直接删除掉。若再需要时,考虑移植或重写这段代码。
这里说的注释掉代码,包括用 /* */ 和 //,还包括 #if 0, #ifdef NEVER_DEFINED 等等。
5 头文件
头文件职责
头文件是模块或文件的对外接口,头文件的设计体现了大部分的系统设计。
头文件中适合放置接口的声明,不适合放置实现(内联函数除外)。对于cpp文件中内部才需要使用的函数、宏、枚举、结构定义等不要放在头文件中。
头文件应当职责单一。头文件过于复杂,依赖过于复杂还是导致编译时间过长的主要原因。
建议5.1.1 每一个.cpp文件应有一个对应的.h文件,用于声明需要对外公开的类与接口
通常情况下,每个.cpp文件都有一个相应的.h,用于放置对外提供的函数声明、宏定义、类型定义等。
如果一个.cpp文件不需要对外公布任何接口,则其就不应当存在。
例外:程序的入口(如main函数所在的文件),单元测试代码,动态库代码。
示例:
// Foo.h
#ifndef FOO_H
#define FOO_H
class Foo {
public:
Foo();
void Fun();
private:
int value_;
};
#endif
// Foo.cpp
#include "Foo.h"
namespace { // Good: 对内函数的声明放在.cpp文件的头部,并声明为匿名namespace或者static限制其作用域
void Bar()
{
}
}
...
void Foo::Fun()
{
Bar();
}
头文件依赖
规则5.2.1 禁止头文件循环依赖
头文件循环依赖,指 a.h 包含 b.h,b.h 包含 c.h,c.h 包含 a.h, 导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。
而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。
头文件循环依赖直接体现了架构设计上的不合理,可通过优化架构去避免。
规则5.2.2 头文件必须编写#define
保护,防止重复包含
为防止头文件被重复包含,所有头文件都应当使用 #define 保护;不要使用 #pragma once
定义包含保护符时,应该遵守如下规则:
1)保护符使用唯一名称;
2)不要在受保护部分的前后放置代码或者注释,文件头注释除外。
示例:假定timer模块的timer.h,其目录为timer/include/timer.h,应按如下方式保护:
#ifndef TIMER_INCLUDE_TIMER_H
#define TIMER_INCLUDE_TIMER_H
...
#endif
规则5.2.3 禁止通过声明的方式引用外部函数接口、变量
只能通过包含头文件的方式使用其他模块或文件提供的接口。
通过 extern 声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。
同时这种隐式依赖,容易导致架构腐化。
不符合规范的案例:
// a.cpp内容
extern int Fun(); // Bad: 通过extern的方式使用外部函数
void Bar()
{
int i = Fun();
...
}
// b.cpp内容
int Fun()
{
// Do something
}
应该改为:
// a.cpp内容
#include "b.h" // Good: 通过包含头文件的方式使用其他.cpp提供的接口
void Bar()
{
int i = Fun();
...
}
// b.h内容
int Fun();
// b.cpp内容
int Fun()
{
// Do something
}
例外,有些场景需要引用其内部函数,但并不想侵入代码时,可以 extern 声明方式引用。
如:
针对某一内部函数进行单元测试时,可以通过 extern 声明来引用被测函数;
当需要对某一函数进行打桩、打补丁处理时,允许 extern 声明该函数。
规则5.2.4 禁止在extern "C"中包含头文件
在 extern "C" 中包含头文件,有可能会导致 extern "C" 嵌套,部分编译器对 extern "C" 嵌套层次有限制,嵌套层次太多会编译错误。
在C,C++混合编程的情况下,在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏,比如链接规范被不正确地更改。
示例,存在a.h和b.h两个头文件:
// a.h内容
...
#ifdef __cplusplus
void Foo(int);
#define A(value) Foo(value)
#else
void A(int)
#endif
// b.h内容
...
#ifdef __cplusplus
extern "C" {
#endif
#include "a.h"
void B();
#ifdef __cplusplus
}
#endif
使用C++预处理器展开b.h,将会得到
extern "C" {
void Foo(int);
void B();
}
按照 a.h 作者的本意,函数 Foo 是一个 C++ 自由函数,其链接规范为 "C++"。
但在 b.h 中,由于 #include "a.h"
被放到了 extern "C"
的内部,函数 Foo 的链接规范被不正确地更改了。
例外:
如果在 C++ 编译环境中,想引用纯C的头文件,这些C头文件并没有extern "C"
修饰。非侵入式的做法是,在 extern "C"
中去包含C头文件。
建议5.2.1尽量避免使用前置声明,而是通过#include
来包含头文件
前置声明(forward declaration)通常指类、模板的纯粹声明,没伴随着其定义。
- 优点:
- 前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。
- 前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。
- 缺点:
- 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
- 前置声明可能会被库的后续更改所破坏。前置声明模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
- 前置声明来自命名空间
std::
的 symbol 时,其行为未定义(在C++11标准规范中明确说明)。
- 前置声明了不少来自头文件的 symbol 时,就会比单单一行的 include 冗长。
- 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂。
- 很难判断什么时候该用前置声明,什么时候该用
#include
,某些场景下面前置声明和#include
互换以后会导致意想不到的结果。
所以我们尽可能避免使用前置声明,而是使用#include头文件来保证依赖关系。
6 作用域
命名空间
建议6.1.1 对于cpp文件中不需要导出的变量,常量或者函数,请使用匿名namespace封装或者用static修饰
在C++ 2003标准规范中,使用static修饰文件作用域的变量,函数等被标记为deprecated特性,所以更推荐使用匿名namespace。
主要原因如下:
- static在C++中已经赋予了太多的含义,静态函数成员变量,静态成员函数,静态全局变量,静态函数局部变量,每一种都有特殊的处理。
- static只能保证变量,常量和函数的文件作用域,但是namespace还可以封装类型等。
- 统一namespace来处理C++的作用域,而不需要同时使用static和namespace来管理。
- static修饰的函数不能用来实例化模板,而匿名namespace可以。
但是不要在 .h 中使用中使用匿名namespace或者static。
// Foo.cpp
namespace {
const int MAX_COUNT = 20;
void InternalFun() {};
}
void Foo::Fun()
{
int i = MAX_COUNT;
InternalFun();
}
规则6.1.1 不要在头文件中或者#include之前使用using导入命名空间
说明:使用using导入命名空间会影响后续代码,易造成符号冲突,所以不要在头文件以及源文件中的#include之前使用using导入命名空间。
示例:
// 头文件a.h
namespace NamespaceA {
int Fun(int);
}
// 头文件b.h
namespace NamespaceB {
int Fun(int);
}
using namespace NamespaceB;
void G()
{
Fun(1);
}
// 源代码a.cpp
#include "a.h"
using namespace NamespaceA;
#include "b.h"
void main()
{
G(); // using namespace NamespaceA在#include “b.h”之前,引发歧义:NamespaceA::Fun,NamespaceB::Fun调用不明确
}
对于在头文件中使用using导入单个符号或定义别名,允许在模块自定义名字空间中使用,但禁止在全局名字空间中使用。
// foo.h
#include <fancy/string>
using fancy::string; // Bad,禁止向全局名字空间导入符号
namespace Foo {
using fancy::string; // Good,可以在模块自定义名字空间中导入符号
using MyVector = fancy::vector<int>; // Good,C++11可在自定义名字空间中定义别名
}
全局函数和静态成员函数
建议6.2.1 优先使用命名空间来管理全局函数,如果和某个class有直接关系的,可以使用静态成员函数
说明:非成员函数放在名字空间内可避免污染全局作用域, 也不要用类+静态成员方法来简单管理全局函数。 如果某个全局函数和某个类有紧密联系, 那么可以作为类的静态成员函数。
如果你需要定义一些全局函数,给某个cpp文件使用,那么请使用匿名namespace来管理。
namespace MyNamespace {
int Add(int a, int b);
}
class File {
public:
static File CreateTempFile(const std::string& fileName);
};
全局常量和静态成员常量
建议6.3.1 优先使用命名空间来管理全局常量,如果和某个class有直接关系的,可以使用静态成员常量
说明:全局常量放在命名空间内可避免污染全局作用域, 也不要用类+静态成员常量来简单管理全局常量。 如果某个全局常量和某个类有紧密联系, 那么可以作为类的静态成员常量。
如果你需要定义一些全局常量,只给某个cpp文件使用,那么请使用匿名namespace来管理。
namespace MyNamespace {
const int MAX_SIZE = 100;
}
class File {
public:
static const std::string SEPARATOR;
};
全局变量
建议6.4.1 尽量避免使用全局变量,考虑使用单例模式
说明:全局变量是可以修改和读取的,那么这样会导致业务代码和这个全局变量产生数据耦合。
int g_counter = 0;
// a.cpp
g_counter++;
// b.cpp
g_counter++;
// c.cpp
cout << g_counter << endl;
使用单实例模式
class Counter {
public:
static Counter& GetInstance()
{
static Counter counter;
return counter;
} // 单实例实现简单举例
void Increase()
{
value_++;
}
void Print() const
{
std::cout << value_ << std::endl;
}
private:
Counter() : value_(0) {}
private:
int value_;
};
// a.cpp
Counter::GetInstance().Increase();
// b.cpp
Counter::GetInstance().Increase();
// c.cpp
Counter::GetInstance().Print();
实现单例模式以后,实现了全局唯一一个实例,和全局变量同样的效果,并且单实例提供了更好的封装性。
例外:有的时候全局变量的作用域仅仅是模块内部,这样进程空间里面就会有多个全局变量实例,每个模块持有一份,这种场景下是无法使用单例模式解决的。
7 类
构造,拷贝构造,赋值和析构函数
构造,拷贝,移动和析构函数提供了对象的生命周期管理方法:
- 构造函数(constructor):
X()
- 拷贝构造函数(copy constructor):
X(const X&)
- 拷贝赋值操作符(copy assignment):
operator=(const X&)
- 移动构造函数(move constructor):
X(X&&)
C++11以后提供
- 移动赋值操作符(move assignment):
operator=(X&&)
C++11以后提供
- 析构函数(destructor):
~X()
规则7.1.1 类的成员变量必须显式初始化
说明:如果类有成员变量,没有定义构造函数,又没有定义默认构造函数,编译器将自动生成一个构造函数,但编译器生成的构造函数并不会对成员变量进行初始化,对象状态处于一种不确定性。
例外:
- 如果类的成员变量具有默认构造函数,那么可以不需要显式初始化。
示例:如下代码没有构造函数,私有数据成员无法初始化:
class Message {
public:
void ProcessOutMsg()
{
//…
}
private:
unsigned int msgID_;
unsigned int msgLength_;
unsigned char* msgBuffer_;
std::string someIdentifier_;
};
Message message; // message成员变量没有初始化
message.ProcessOutMsg(); // 后续使用存在隐患
// 因此,有必要定义默认构造函数,如下:
class Message {
public:
Message() : msgID_(0), msgLength_(0), msgBuffer_(nullptr)
{
}
void ProcessOutMsg()
{
// …
}
private:
unsigned int msgID_;
unsigned int msgLength_;
unsigned char* msgBuffer_;
std::string someIdentifier_; // 具有默认构造函数,不需要显式初始化
};
建议7.1.1 成员变量优先使用声明时初始化(C++11)和构造函数初始化列表初始化
说明:C++11的声明时初始化可以一目了然的看出成员初始值,应当优先使用。如果成员初始化值和构造函数相关,或者不支持C++11,则应当优先使用构造函数初始化列表来初始化成员。相比起在构造函数体中对成员赋值,初始化列表的代码更简洁,执行性能更好,而且可以对const成员和引用成员初始化。
class Message {
public:
Message() : msgLength_(0) // Good,优先使用初始化列表
{
msgBuffer_ = nullptr; // Bad,不推荐在构造函数中赋值
}
private:
unsigned int msgID_{0}; // Good,C++11中使用
unsigned int msgLength_;
unsigned char* msgBuffer_;
};
规则7.1.2 为避免隐式转换,将单参数构造函数声明为explicit
说明:单参数构造函数如果没有用explicit声明,则会成为隐式转换函数。
示例:
class Foo {
public:
explicit Foo(const string& name): name_(name)
{
}
private:
string name_;
};
void ProcessFoo(const Foo& foo){}
int main(void)
{
std::string test = "test";
ProcessFoo(test); // 编译不通过
return 0;
}
上面的代码编译不通过,因为ProcessFoo
需要的参数是Foo类型,传入的string类型不匹配。
如果将Foo构造函数的explicit关键字移除,那么调用ProcessFoo
传入的string就会触发隐式转换,生成一个临时的Foo对象。往往这种隐式转换是让人迷惑的,并且容易隐藏Bug,得到了一个不期望的类型转换。所以对于单参数的构造函数是要求explicit声明。
规则7.1.3 如果不需要拷贝构造函数、赋值操作符 / 移动构造函数、赋值操作符,请明确禁止
说明:如果用户不定义,编译器默认会生成拷贝构造函数和拷贝赋值操作符, 移动构造和移动赋值操作符(移动语义的函数C++11以后才有)。
如果我们不要使用拷贝构造函数,或者赋值操作符,请明确拒绝:
- 将拷贝构造函数或者赋值操作符设置为private,并且不实现:
class Foo {
private:
Foo(const Foo&);
Foo& operator=(const Foo&);
};
- 使用C++11提供的delete, 请参见后面现代C++的相关章节。
- 推荐继承NoCopyable、NoMovable,禁止使用DISALLOW_COPY_AND_MOVE,DISALLOW_COPY,DISALLOW_MOVE等宏。
class Foo : public NoCopyable, public NoMovable {
};
NoCopyable和NoMovable的实现:
class NoCopyable {
public:
NoCopyable() = default;
NoCopyable(const NoCopyable&) = delete;
NoCopyable& operator = (NoCopyable&) = delete;
};
class NoMovable {
public:
NoMovable() = default;
NoMovable(NoMovable&&) noexcept = delete;
NoMovable& operator = (NoMovable&&) noexcept = delete;
};
规则7.1.4 拷贝构造和拷贝赋值操作符应该是成对出现或者禁止
拷贝构造函数和拷贝赋值操作符都是具有拷贝语义的,应该同时出现或者禁止。
// 同时出现
class Foo {
public:
...
Foo(const Foo&);
Foo& operator=(const Foo&);
...
};
// 同时default, C++11支持
class Foo {
public:
Foo(const Foo&) = default;
Foo& operator=(const Foo&) = default;
};
// 同时禁止, C++11可以使用delete
class Foo {
private:
Foo(const Foo&);
Foo& operator=(const Foo&);
};
规则7.1.5 移动构造和移动赋值操作符应该是成对出现或者禁止
在C++11中增加了move操作,如果需要某个类支持移动操作,那么需要实现移动构造和移动赋值操作符。
移动构造函数和移动赋值操作符都是具有移动语义的,应该同时出现或者禁止。
// 同时出现
class Foo {
public:
...
Foo(Foo&&);
Foo& operator=(Foo&&);
...
};
// 同时default, C++11支持
class Foo {
public:
Foo(Foo&&) = default;
Foo& operator=(Foo&&) = default;
};
// 同时禁止, 使用C++11的delete
class Foo {
public:
Foo(Foo&&) = delete;
Foo& operator=(Foo&&) = delete;
};
规则7.1.6 禁止在构造函数和析构函数中调用虚函数
说明:在构造函数和析构函数中调用当前对象的虚函数,会导致未实现多态的行为。
在C++中,一个基类一次只构造一个完整的对象。
示例:类Base是基类,Sub是派生类
class Base {
public:
Base();
virtual void Log() = 0; // 不同的派生类调用不同的日志文件
};
Base::Base() // 基类构造函数
{
Log(); // 调用虚函数Log
}
class Sub : public Base {
public:
virtual void Log();
};
当执行如下语句:
Sub sub;
会先执行Sub的构造函数,但首先调用Base的构造函数,由于Base的构造函数调用虚函数Log,此时Log还是基类的版本,只有基类构造完成后,才会完成派生类的构造,从而导致未实现多态的行为。
同样的道理也适用于析构函数。
规则7.1.7 多态基类中的拷贝构造函数、拷贝赋值操作符、移动构造函数、移动赋值操作符必须为非public函数或者为delete函数
如果报一个派生类对象直接赋值给基类对象,会发生切片,只拷贝或者移动了基类部分,损害了多态行为。
【反例】
如下代码中,基类没有定义拷贝构造函数或拷贝赋值操作符,编译器会自动生成这两个特殊成员函数,
如果派生类对象赋值给基类对象时就发生切片。可以将此例中的拷贝构造函数和拷贝赋值操作符声明为delete,编译器可检查出此类赋值行为。
class Base {
public:
Base() = default;
virtual ~Base() = default;
...
virtual void Fun() { std::cout << "Base" << std::endl;}
};
class Derived : public Base {
...
void Fun() override { std::cout << "Derived" << std::endl; }
};
void Foo(const Base &base)
{
Base other = base; // 不符合:发生切片
other.Fun(); // 调用的时Base类的Fun函数
}
Derived d;
Foo(d); // 传入的是派生类对象
- 将拷贝构造函数或者赋值操作符设置为private,并且不实现:
继承
规则7.2.1 基类的析构函数应该声明为virtual,不准备被继承的类需要声明为final
说明:只有基类析构函数是virtual,通过多态调用的时候才能保证派生类的析构函数被调用。
示例:基类的析构函数没有声明为virtual导致了内存泄漏。
class Base {
public:
virtual std::string getVersion() = 0;
~Base()
{
std::cout << "~Base" << std::endl;
}
};
class Sub : public Base {
public:
Sub() : numbers_(nullptr)
{
}
~Sub()
{
delete[] numbers_;
std::cout << "~Sub" << std::endl;
}
int Init()
{
const size_t numberCount = 100;
numbers_ = new (std::nothrow) int[numberCount];
if (numbers_ == nullptr) {
return -1;
}
...
}
std::string getVersion()
{
return std::string("hello!");
}
private:
int* numbers_;
};
int main(int argc, char* args[])
{
Base* b = new Sub();
delete b;
return 0;
}
由于基类Base的析构函数没有声明为virtual,当对象被销毁时,只会调用基类的析构函数,不会调用派生类Sub的析构函数,导致内存泄漏。
例外:
NoCopyable、NoMovable这种没有任何行为,仅仅用来做标识符的类,可以不定义虚析构也不定义final。
规则7.2.2 禁止虚函数使用缺省参数值
说明:在C++中,虚函数是动态绑定的,但函数的缺省参数却是在编译时就静态绑定的。这意味着你最终执行的函数是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。为了避免虚函数重载时,因参数声明不一致给使用者带来的困惑和由此导致的问题,规定所有虚函数均不允许声明缺省参数值。
示例:虚函数display缺省参数值text是由编译时刻决定的,而非运行时刻,没有达到多态的目的:
class Base {
public:
virtual void Display(const std::string& text = "Base!")
{
std::cout << text << std::endl;
}
virtual ~Base(){}
};
class Sub : public Base {
public:
virtual void Display(const std::string& text = "Sub!")
{
std::cout << text << std::endl;
}
virtual ~Sub(){}
};
int main()
{
Base* base = new Sub();
Sub* sub = new Sub();
...
base->Display(); // 程序输出结果: Base! 而期望输出:Sub!
sub->Display(); // 程序输出结果: Sub!
delete base;
delete sub;
return 0;
};
规则7.2.3 禁止重新定义继承而来的非虚函数
说明:因为非虚函数无法实现动态绑定,只有虚函数才能实现动态绑定:只要操作基类的指针,即可获得正确的结果。
示例:
class Base {
public:
void Fun();
};
class Sub : public Base {
public:
void Fun();
};
Sub* sub = new Sub();
Base* base = sub;
sub->Fun(); // 调用子类的Fun
base->Fun(); // 调用父类的Fun
//...
(未完待续)
转载自:https://gitee.com/openharmony/docs/blob/master/zh-cn/contribute/OpenHarmony-cpp-coding-style-guide.md#