360代码规范六:declaration
360代码规范六:declaration
Hoshea ZhangNaming
遵循合理的命名方式
应遵循易于读写,并可准确表达代码意图的命名方式
不应出现下列情况:
- 超长的名称
- 易造成混淆或冲突的名称
- 无意义或意义过于空泛的名称
- 不易于读写的名称
- 有违公序良俗的名称
示例:
1 | int xxx(int); // Bad, meaningless name |
例中 xxx、fun 这种无意义或意义过于空泛的名称,以及 l、lI、N0 这种易与数字或其他单词混淆的名称均是不符合要求的;Unicode 转义名称只应出现在字符串中,否则没有可读性;名称中各单词间应有下划线或大小写变化,否则不便于读写。本规则集合示例中出现的 foo、bar 等名称,意在代指一般的代码元素,仅作示例,实际代码中不应出现。
不良命名方式甚至会导致标准未定义的行为,如:
1 | extern int identifier_of_a_very_very_long_name_1; |
注意,如果两个名称有相同的前缀,而且相同前缀超过一定长度时是危险的,有可能会导致编译器无法有效区分相关名称。C 标准指明,保证名称前 31 位不同即可避免这种问题,可参见 ISO/IEC 9899:2011 5.2.4.1 的相关规定。
不建议采用相同“长前缀”+ 不同“短后缀”的命名方式,这种名称非常容易形成笔误或由复制粘贴造成错误,如:
1 | struct BinExpr { |
设 BinExpr 是“二元表达式”类,sub0、sub1 为左右子表达式,这种命名方式应改进:
1 | struct BinExpr { |
不应定义具有保留意义的名称
自定义的名称不应与标准库或编译环境中的名称相同,否则会导致标准未定义的行为,也不利于阅读和维护。
下列名称具有保留意义,自定义名称不应与之相同:
- 标准库或编译环境中的宏名称
- 标准库中具有外部链接性的对象或函数名称
- 标准库中的类型名称
自定义字面常量后缀应以下划线开头,否则为保留名称,除此之外:
- 以两个下划线开头的名称
- 以一个下划线和一个大写字母开头的名称
- 以下划线开头的全局名称
均具有保留意义,自定义名称应避免这种命名方式。
对于宏,本规则特化为 ID_macro_defineReserved、ID_macro_undefReserved。
示例:
1 | #include <errno.h> |
例中成员变量 errno 与标准库中的 errno 名称相同,不便于区分是自定义的还是系统定义的。
又如:
1 | size_t _Size(); // Non-compliant |
例中函数名 _Size 以一个下划线和一个大写字母开头,自定义字面常量后缀 KB 未以下划线开头,均不符合要求。
为避免冲突和误解,以下命名方式可供参考:
- 除自定义字面常量后缀之外,避免名称以下划线开头
- 无命名空间限制的全局名称以模块名称开头
- 从名称上体现作用域,如全局对象名以 g_ 开头,成员对象名以 m_ 开头或以 _ 结尾
- 从名称上体现类别,如宏名采用全大写字母,类型名以大写字母开头,函数或对象名以小写字母开头
本规则集合对具体的命名方式暂不作量化要求,但读者应具备相关意识。
局部名称不应被覆盖
不应在嵌套的作用域中声明相同的名称,否则干扰阅读,极易引起误解。
示例:
1 | int foo() { |
在一个函数中出现了多个名为 i 的变量,当实际代码较为复杂时,很容易出现意图与实现不符的问题。
成员名称不应被覆盖
成员函数内的局部名称与成员名称相同会干扰阅读,易引起误解。
示例:
1 | class A { |
建议成员对象遵循统一的命名约定,如以“_”结尾或以“m_”开头,可有效规避这类问题:
1 | class A { |
全局名称不应被覆盖
局部、成员名称不应与全局或命名空间内的名称相同,否则干扰阅读,易引起误解。
示例:
1 | extern int i; |
建议全局对象遵循统一的命名约定,如以“g_”开头,且名称长度不宜过短,可有效规避这类问题。
例外:
1 | extern int i; |
无成员函数的结构体或联合体成员可不受本规则限制。
类型名称不应重复定义
如果类型相关的名称有重复,极易引起误解,不利于阅读和维护,对于:
- C++ 类、联合体、枚举类型的名称
- C 结构体、联合体、枚举类型的标签名称
- 用 typedef 或 using 定义的类型别名
均不应重复定义。
示例:
1 | typedef double A; |
例外:
1 | namespace N { |
如果类型定义处于不同的命名空间,可不受本规则约束。
类型名称不应与对象或函数名称相同
不同的代码元素使用相同的名称不利于阅读和维护。
示例:
1 | struct A { |
例中结构体名称 A 与枚举项 A 重名,sizeof(A) 的意义是非常令人困惑的。
不应出现拼写错误
代码中不应存在拼写错误,尤其是供他人调用的代码,如命名空间名称、公共接口名称等,更不应存在拼写错误。
拼写错误会使用户对代码的质量产生疑虑,而且相关代码被大量引用后也不便于改正。
示例:
1 | class A { |
例中“destory”函数的名称有拼写错误,应改为“destroy”。
Qualifier
const volatile不应重复
重复的 const 或 volatile 限定符是没意义的,很可能意味着某种错误。
示例:
1 | const const char* p0 = "...."; // Non-compliant |
对于 p0 和 p1,const 重复限定 char,其中一个 const 很可能是为了限定 * 号,但形成了笔误,应改为:
1 | const char * const p0 = "...."; // Compliant |
对于 p2,const 重复限定 * 号,其中一个 const 很可能是为了限定 char,应改为:
1 | const char * const p2 = "...."; // Compliant |
复习一下:
const int p= int const p 修饰int不能修改值
int *const p 修饰p,不能修改指向的地址
const、volatile限定指针类型的别名是可疑的
如果用 const、volatile 限定指针类型的别名,很可能会造成意料之外的错误。
示例:
1 | struct Type { |
例中 Alias 是 Type* 的别名,“const Alias a”很容易引起误解,好像对象是不可被改变的,但实际上 a 的类型是 Type *const,const 限定的是指针而不是指针指向的对象,对象仍可被修改,其调用的函数也可能与预期不符。
应避免为指针类型定义别名,否则应提供常量和非常量两种别名,如:
1 | typedef Type* Alias; |
注意,如果用 const、volatile 限定引用的别名则是错误的,详见 ID_qualifierInvalid。
const/volatile不能限定引用
在 C++ 语言中,const 或 volatile 可以限定指针,但不可限定引用,否则起不到任何作用。
示例:
1 | int a = 0; |
限定 & 号的 const 和 volatile 是无效的,i 可被随意修改,j 也可能被优化。
应去掉限定符,或使限定符作用于类型名称:
1 | const int& i = a; // Compliant |
注意,如果限定符作用于引用类型的别名,会引起很大误解,如:
1 | typedef int& int_r; // Reference type alias, bad |
例中 r0 像是一个常量对象,而 r1 像是常量对象的引用,但 const int_r 展开后相当于 int & const,r0 不是常量,r1 也不是常量的引用。
const、volatile 限定类型时的位置应统一
语言允许 const、volatile 等关键字出现在类型名称的左侧,也可以出现在其右侧,甚至可以出现在基本类型名称的中间,应对其位置进行统一规范以提高可读性。
可从下列方案中选择一种作为规范,即统一要求 const、volatile:
- 出现在类型名称的左侧
- 出现在类型名称的右侧
- 出现在指针类型名称的右侧,非指针类型名称的左侧
指向常量字符串的指针应使用const声明
常量字符串与非常量字符串指针的隐式转换是不安全的,一旦相关内存被修改会导致标准未定义的行为,这种转换在 C++ 标准中是过时的,在 C 代码中也不应出现。
指向常量字符串的指针应声明为 const chartype *,chartype 为常量字符串中的字符类型,如:
1 | char、wchar_t、char16_t、char32_t |
示例:
1 | char* p = "...."; // Non-compliant |
例中非常量指针 p 指向常量字符串,通过 p 修改常量数据一般会引发“段错误”而导致崩溃,应改为:
1 | const char* p = "...."; // Compliant |
改为常量字符串指针后,错误的操作无法通过编译。
又如:
1 | void foo(char*); |
应将 foo 的参数类型改为 const char*,或将常量字符串复制后传给 foo 函数。
枚举类型不应为const或者volatile
将 enum 或 enum class 的底层类型(underlying type)设为 const 或 volatile 是没有意义的,会被编译器忽略,属于语言运用错误。
对常量的定义不应为引用
虽然 C++ 语言十分灵活,可以通过多种方式达到同一种目的,但应该选择最简洁且通俗易懂的方式实现。
示例:
1 | const int& i = 1024; // Non-compliant |
应改为:
1 | const int i = 1024; // Compliant |
相关对象未被修改时应使用const声明
用 const 显式区分数据是只读的还是可写的,细化数据的访问方式可显著提高可读性,并保护数据不被错误修改,有助于编译器优化。
下列情况应使用 const 声明:
- 不需要被修改的非参数对象应声明为常量对象
- 通过指针或引用访问对象但不修改对象时,应声明为常量指针或引用
- 成员函数访问非静态成员对象但不修改相关对象时,应声明为常量成员函数
示例:
1 | double pi = 3.14; // Non-compliant |
例中 pi 未被修改,应作为常量;拷贝构造函数的参数未被修改,应声明为常量引用;成员函数 area 未修改成员对象,应声明为常量成员函数:
1 | const double pi = 3.14; // Compliant |
Specifier
合理使用auto关键字
auto 关键字隐藏了类型名称,在使用时需注意不应降低可读性。
非局部对象不宜用 auto 声明,如接口的返回类型、参数、全局对象等。如果局部对象的类型对程序的行为有显著影响,也不宜用 auto 声明。
示例:
1 | auto foo() { |
如果想确定 obj 对象的类型,必须通读所有与之相关的代码,可读性很差。
将代码中所有可以替换成 auto 的标识符全部替换成 auto,其结果是不可想象的,与 Python 等语言不同,C++ 语言存在重载、模板等多种严格依赖于类型的特性,如果类型名称不明确,必然会造成阅读和维护等方面的障碍。
下面给出 auto 关键字的合理用法:
1 | Type* a = static_cast<Type*>(ptr); // Repeated type name |
重复的类型名称使代码变得繁琐,这种情况使用 auto 是更好的方法:
1 | auto* a = static_cast<Type*>(ptr); // OK |
又如:
1 | vector<Type> v{ .... }; |
begin 函数返回迭代器是一种常识,且迭代器类型名称往往较长,这种情况应使用 auto:
1 | auto i = v.begin(); // OK |
又如:
1 | struct SomeClass { |
重复的类作用域声明十分繁琐,可用 auto 关键字配合后置返回类型改善:
1 | auto SomeClass::foo() -> Sub { // OK |
总之,使用 auto 关键字的目的应是提高可读性,而不是单纯地简化代码。
不应使用已过时的关键字
在 C++11 标准中,register 关键字已过时,auto 关键字也不可再作为“存储类说明符(storage class specifier)”。
本规则对 C++ 代码适用,C 代码可不受限制。
示例:
1 | register int a; // Non-compliant |
不应使用多余的inline关键词
由 constexpr 关键字限定的函数已经相当于被声明为 inline,不应再重复声明。
示例:
1 | inline constexpr int foo(int n) { // Non-compliant, ‘inline’ is redundant |
应改为:
1 | constexpr int foo(int n) { // Compliant |
另外,在类声明中实现的函数也相当于被声明为 inline,不应重复声明:
1 | class A { |
extern 关键字不应作用于类成员的声明或定义
extern 关键字作用于类成员的声明或定义是没有意义的,属于语言运用错误。
示例:
1 | class A { |
重写的虚函数应声明为 override 或 final
将重写的虚函数都声明为 override 或 final 有利于提高可读性,并可确保虚函数被有效重写。
示例:
1 | class A { |
例中 B 重写 A 的 foo 和 bar 这两个虚函数,如果不看 A 的声明,则看不出 B::foo 是虚函数,也看不出 B::bar 是重写的虚函数。
改为如下方式会清晰很多:
1 | class B: public A { |
而且当重写的函数名、参数、返回类型与基类声明不符时,不能通过编译,可及时修正问题。
final
下面是一些 final 关键字的使用示例:
1、用 final 修饰类,表示该类不能被继承:
1
2
3
4
5
6
7
8
9
10class Base final {
public:
// ...
};
// 错误示例,无法继承 final 类
class Derived : public Base {
public:
// ...
};2、用 final 修饰虚函数,表示该虚函数不能被子类重写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Base {
public:
virtual void foo() final {
// ...
}
};
class Derived : public Base {
public:
// 错误示例,无法重写 final 函数
virtual void foo() {
// ...
}
};3、用 final 修饰成员函数,表示该成员函数不能在子类中被重写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Base {
public:
void foo() final {
// ...
}
};
class Derived : public Base {
public:
// 错误示例,无法重写 final 函数
void foo() {
// ...
}
};
override 和 final 关键字不应同时出现在声明中
final 表示不可重写的重写,override 表示可再次重写的重写,这两个关键字不应同时出现在声明中。
示例:
1 | class D: public B { |
override 或 final 关键字不应与 virtual 关键字同时出现在声明中
只应在定义新的虚函数时使用 virtual 关键字,重写虚函数应使用 override 或 final 关键字,不应再出现 virtual 关键字。
示例:
1 | class A { |
去掉多余的 virtual 关键字使代码更简洁:
1 | class B: public A { |
不应将union设为final
在 C++ 语言中,union 不可作为基类,将 union 声明为 final 是没有意义的,属于语言运用错误。
示例:
1 | union U final // Non-compliant, meaningless |
未访问 this 指针的成员函数应使用 static 声明
如果未访问 this 指针的成员函数没有被设计为静态成员函数,很可能意味着错误或功能不完整。
示例:
1 | class A { |
例中 foo 函数只访问了静态数据成员,但在调用时仍会将 this 指针作为参数,这在逻辑上是矛盾的,所以应使用 static 关键字明确声明。
声明和定义内部链接的对象和函数时均应使用 static 关键字
声明和定义内部链接的对象和函数时均应使用 static 关键字,不可使用 extern 关键字,否则极易引起误解。
示例:
1 | extern int a; // Non-compliant |
例中 a、b 是内部链接的静态对象,在定义的前后不可再用 extern 声明,否则极易与全域对象混淆。
又如:
1 | int foo(int); // Bad, missing ‘static’ |
在声明和定义内部链接的函数时,均应使用 static 关键字,否则也易引起误解。
inline、virtual、static、typedef 等关键字的位置应统一
语言允许 inline、virtual、static、typedef 等关键字出现在类型名称的左侧,也可以出现在其右侧,甚至可以出现在基本类型名称的中间,应对其位置进行统一规范以提高可读性。
本规则对下列 C 或 C++ 关键字有同样的要求:
1 | inline、virtual、explicit、 |
这些关键字应统一出现在声明的起始,类型名称的左侧。
对于 const 和 volatile 也需面对类似的问题,参见 ID_badQualifierPosition。
示例:
1 | struct A { |
例中各种声明均有一定的特殊性,如果声明其特殊性的关键字在类型名称之后,不便于阅读甚至会引起误解。
应改为:
1 | struct A { |
未完待续