360代码规范六:declaration

Naming

遵循合理的命名方式

应遵循易于读写,并可准确表达代码意图的命名方式

不应出现下列情况:

  • 超长的名称
  • 易造成混淆或冲突的名称
  • 无意义或意义过于空泛的名称
  • 不易于读写的名称
  • 有违公序良俗的名称

示例:

1
2
3
4
5
6
7
8
9
10
11
int xxx(int);   // Bad, meaningless name

int fun(int); // Bad, vague name

int l, I, O, l0, Il; // Bad, like numbers

int YE5, N0; // Bad, like a word but not

int \u540d\u79f0; // Bad, no readability

int nVarietyisthespiceoflife = 123; // Bad, hard to read or write

例中 xxx、fun 这种无意义或意义过于空泛的名称,以及 l、lI、N0 这种易与数字或其他单词混淆的名称均是不符合要求的;Unicode 转义名称只应出现在字符串中,否则没有可读性;名称中各单词间应有下划线或大小写变化,否则不便于读写。本规则集合示例中出现的 foo、bar 等名称,意在代指一般的代码元素,仅作示例,实际代码中不应出现。

不良命名方式甚至会导致标准未定义的行为,如:

1
2
extern int identifier_of_a_very_very_long_name_1;
extern int identifier_of_a_very_very_long_name_2; // Dangerous

注意,如果两个名称有相同的前缀,而且相同前缀超过一定长度时是危险的,有可能会导致编译器无法有效区分相关名称。C 标准指明,保证名称前 31 位不同即可避免这种问题,可参见 ISO/IEC 9899:2011 5.2.4.1 的相关规定。

不建议采用相同“长前缀”+ 不同“短后缀”的命名方式,这种名称非常容易形成笔误或由复制粘贴造成错误,如:

1
2
3
4
struct BinExpr {
BinExpr* sub0; // Bad
BinExpr* sub1; // Bad
};

设 BinExpr 是“二元表达式”类,sub0、sub1 为左右子表达式,这种命名方式应改进:

1
2
3
4
struct BinExpr {
BinExpr* left; // Better
BinExpr* right; // Better
};

不应定义具有保留意义的名称

自定义的名称不应与标准库或编译环境中的名称相同,否则会导致标准未定义的行为,也不利于阅读和维护。

下列名称具有保留意义,自定义名称不应与之相同:

  • 标准库或编译环境中的宏名称
  • 标准库中具有外部链接性的对象或函数名称
  • 标准库中的类型名称

自定义字面常量后缀应以下划线开头,否则为保留名称,除此之外:

  • 以两个下划线开头的名称
  • 以一个下划线和一个大写字母开头的名称
  • 以下划线开头的全局名称

均具有保留意义,自定义名称应避免这种命名方式。

对于宏,本规则特化为 ID_macro_defineReserved、ID_macro_undefReserved。

示例:

1
2
3
4
5
6
7
8
9
10
11
#include <errno.h>

struct A {
void foo() {
if (errno != 0) { // Which errno?
....
}
}
private:
int errno; // Non-compliant
};

例中成员变量 errno 与标准库中的 errno 名称相同,不便于区分是自定义的还是系统定义的。

又如:

1
2
3
4
5
size_t _Size();   // Non-compliant

size_t operator "" KB(unsigned long long n) { // Non-compliant
return n * 1024;
}

例中函数名 _Size 以一个下划线和一个大写字母开头,自定义字面常量后缀 KB 未以下划线开头,均不符合要求。

为避免冲突和误解,以下命名方式可供参考:

  • 除自定义字面常量后缀之外,避免名称以下划线开头
  • 无命名空间限制的全局名称以模块名称开头
  • 从名称上体现作用域,如全局对象名以 g_ 开头,成员对象名以 m_ 开头或以 _ 结尾
  • 从名称上体现类别,如宏名采用全大写字母,类型名以大写字母开头,函数或对象名以小写字母开头

本规则集合对具体的命名方式暂不作量化要求,但读者应具备相关意识。

局部名称不应被覆盖

不应在嵌套的作用域中声明相同的名称,否则干扰阅读,极易引起误解。

示例:

1
2
3
4
5
6
7
8
int foo() {
int i = 0; // Declares an object ‘i’
if (cond) {
int i = 1; // Non-compliant, hides previous ‘i’
....
}
return i;
}

在一个函数中出现了多个名为 i 的变量,当实际代码较为复杂时,很容易出现意图与实现不符的问题。

成员名称不应被覆盖

成员函数内的局部名称与成员名称相同会干扰阅读,易引起误解。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
int i = 0; // Member object ‘i’

public:
int foo() {
int i = 0; // Non-compliant, hides the member ‘i’
return bar(i);
}

int bar(int i) { // Non-compliant, hides the member ‘i’
return i + i; // Which ‘i’ is used?
}
};

建议成员对象遵循统一的命名约定,如以“_”结尾或以“m_”开头,可有效规避这类问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
int i_ = 0; // Member object ‘i_’

public:
int foo() {
int i = 0; // Compliant
return bar(i);
}

int bar(int i) { // Compliant
return i_ + i;
}
};

全局名称不应被覆盖

局部、成员名称不应与全局或命名空间内的名称相同,否则干扰阅读,易引起误解。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern int i;

void foo() {
int i = 0; // Non-compliant, hides the global ‘i’
....
}

class A {
int i; // Non-compliant, hides the global ‘i’
public:
int bar() {
return i; // Which ‘i’?
}
....
};

建议全局对象遵循统一的命名约定,如以“g_”开头,且名称长度不宜过短,可有效规避这类问题。

例外:

1
2
3
4
5
extern int i;

struct S {
int i; // Compliant
};

无成员函数的结构体或联合体成员可不受本规则限制。

类型名称不应重复定义

如果类型相关的名称有重复,极易引起误解,不利于阅读和维护,对于:

  • C++ 类、联合体、枚举类型的名称
  • C 结构体、联合体、枚举类型的标签名称
  • 用 typedef 或 using 定义的类型别名

均不应重复定义。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
typedef double A;

void foo() {
typedef float A; // Non-compliant
typedef short B;
....
}

void bar() {
typedef short B; // Non-compliant, even if they are identical
....
}

例外:

1
2
3
4
5
6
namespace N {
typedef double A;
}
namespace M {
typedef float A; // Compliant
}

如果类型定义处于不同的命名空间,可不受本规则约束。

类型名称不应与对象或函数名称相同

不同的代码元素使用相同的名称不利于阅读和维护。

示例:

1
2
3
4
5
6
7
8
9
struct A {
....
};

enum {
A, B, C // Non-compliant
};

size_t x = sizeof(A); // Which ‘A’?

例中结构体名称 A 与枚举项 A 重名,sizeof(A) 的意义是非常令人困惑的。

不应出现拼写错误

代码中不应存在拼写错误,尤其是供他人调用的代码,如命名空间名称、公共接口名称等,更不应存在拼写错误。

拼写错误会使用户对代码的质量产生疑虑,而且相关代码被大量引用后也不便于改正。

示例:

1
2
3
4
class A {
public:
virtual void destory() = 0; // Non-compliant, should be ‘destroy’
};

例中“destory”函数的名称有拼写错误,应改为“destroy”。

Qualifier

const volatile不应重复

重复的 const 或 volatile 限定符是没意义的,很可能意味着某种错误。

示例:

1
2
3
const const char* p0 = "....";   // Non-compliant
const char const* p1 = "...."; // Non-compliant
char* const const p2 = "...."; // Non-compliant

对于 p0 和 p1,const 重复限定 char,其中一个 const 很可能是为了限定 * 号,但形成了笔误,应改为:

1
2
const char * const p0 = "....";  // Compliant
const char * const p1 = "...."; // 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
2
3
4
5
6
7
8
9
10
struct Type {
void foo();
void foo() const;
};

typedef Type* Alias;

void bar(const Alias a) { // Rather suspicious
a->foo(); // Calls ‘void Type::foo();’
}

例中 Alias 是 Type* 的别名,“const Alias a”很容易引起误解,好像对象是不可被改变的,但实际上 a 的类型是 Type *const,const 限定的是指针而不是指针指向的对象,对象仍可被修改,其调用的函数也可能与预期不符。

应避免为指针类型定义别名,否则应提供常量和非常量两种别名,如:

1
2
3
4
5
6
typedef Type* Alias;
typedef const Type* ConstAlias;

void bar(ConstAlias a) {
a->foo(); // Calls ‘void Type::foo() const;’
}

注意,如果用 const、volatile 限定引用的别名则是错误的,详见 ID_qualifierInvalid。

const/volatile不能限定引用

在 C++ 语言中,const 或 volatile 可以限定指针,但不可限定引用,否则起不到任何作用。

示例:

1
2
3
int a = 0;
int &const i = a; // Non-compliant
int &volatile j = a; // Non-compliant

限定 & 号的 const 和 volatile 是无效的,i 可被随意修改,j 也可能被优化。

应去掉限定符,或使限定符作用于类型名称:

1
2
const int& i = a;     // Compliant
volatile int& j = a; // Compliant

注意,如果限定符作用于引用类型的别名,会引起很大误解,如:

1
2
3
typedef int& int_r;   // Reference type alias, bad
const int_r r0 = a; // Non-compliant, r0 is not a const-reference at all
const int_r& r1 = a; // Non-compliant, r1 is not a const-reference at all

例中 r0 像是一个常量对象,而 r1 像是常量对象的引用,但 const int_r 展开后相当于 int & const,r0 不是常量,r1 也不是常量的引用。

const、volatile 限定类型时的位置应统一

语言允许 const、volatile 等关键字出现在类型名称的左侧,也可以出现在其右侧,甚至可以出现在基本类型名称的中间,应对其位置进行统一规范以提高可读性。

可从下列方案中选择一种作为规范,即统一要求 const、volatile:

  1. 出现在类型名称的左侧
  2. 出现在类型名称的右侧
  3. 出现在指针类型名称的右侧,非指针类型名称的左侧

指向常量字符串的指针应使用const声明

常量字符串与非常量字符串指针的隐式转换是不安全的,一旦相关内存被修改会导致标准未定义的行为,这种转换在 C++ 标准中是过时的,在 C 代码中也不应出现。

指向常量字符串的指针应声明为 const chartype *,chartype 为常量字符串中的字符类型,如:

1
char、wchar_t、char16_t、char32_t

示例:

1
2
char* p = "....";   // Non-compliant
p[x] = '\0'; // Undefined behavior

例中非常量指针 p 指向常量字符串,通过 p 修改常量数据一般会引发“段错误”而导致崩溃,应改为:

1
2
const char* p = "....";   // Compliant
p[x] = '\0'; // Compile-time protected

改为常量字符串指针后,错误的操作无法通过编译。

又如:

1
2
3
4
5
void foo(char*);

void bar() {
foo("...."); // Non-compliant
}

应将 foo 的参数类型改为 const char*,或将常量字符串复制后传给 foo 函数。

枚举类型不应为const或者volatile

将 enum 或 enum class 的底层类型(underlying type)设为 const 或 volatile 是没有意义的,会被编译器忽略,属于语言运用错误。

对常量的定义不应为引用

虽然 C++ 语言十分灵活,可以通过多种方式达到同一种目的,但应该选择最简洁且通俗易懂的方式实现。

示例:

1
2
const int& i = 1024;   // Non-compliant
const int&& j = 1024; // Non-compliant

应改为:

1
2
const int i = 1024;  // Compliant
const int j = 1024; // Compliant

相关对象未被修改时应使用const声明

用 const 显式区分数据是只读的还是可写的,细化数据的访问方式可显著提高可读性,并保护数据不被错误修改,有助于编译器优化。

下列情况应使用 const 声明:

  • 不需要被修改的非参数对象应声明为常量对象
  • 通过指针或引用访问对象但不修改对象时,应声明为常量指针或引用
  • 成员函数访问非静态成员对象但不修改相关对象时,应声明为常量成员函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
double pi = 3.14;   // Non-compliant

class Circle {
double r;

public:
Circle(double);
Circle(Circle&) = default; // Non-compliant

double area() { // Non-compliant
return pi * r * r;
}
};

例中 pi 未被修改,应作为常量;拷贝构造函数的参数未被修改,应声明为常量引用;成员函数 area 未修改成员对象,应声明为常量成员函数:

1
2
3
4
5
6
7
8
9
10
const double pi = 3.14;   // Compliant

class Circle {
....
Circle(const Circle&) = default; // Compliant

double area() const { // Compliant
return pi * r * r;
}
};

Specifier

合理使用auto关键字

auto 关键字隐藏了类型名称,在使用时需注意不应降低可读性。

非局部对象不宜用 auto 声明,如接口的返回类型、参数、全局对象等。如果局部对象的类型对程序的行为有显著影响,也不宜用 auto 声明。

示例:

1
2
3
4
5
6
7
8
9
10
11
auto foo() {
....
}

auto bar() {
auto x = foo();
....
return x;
}

auto obj = bar(); // What the hell is ‘obj’??

如果想确定 obj 对象的类型,必须通读所有与之相关的代码,可读性很差。

将代码中所有可以替换成 auto 的标识符全部替换成 auto,其结果是不可想象的,与 Python 等语言不同,C++ 语言存在重载、模板等多种严格依赖于类型的特性,如果类型名称不明确,必然会造成阅读和维护等方面的障碍。

下面给出 auto 关键字的合理用法:

1
2
Type* a = static_cast<Type*>(ptr);               // Repeated type name
unique_ptr<Type[]> b = make_unique<Type[]>(10); // Repeated type name

重复的类型名称使代码变得繁琐,这种情况使用 auto 是更好的方法:

1
2
auto* a = static_cast<Type*>(ptr);  // OK
auto b = make_unique<Type[]>(10); // OK

又如:

1
2
vector<Type> v{ .... };
vector<Type>::iterator i = v.begin(); // Verbose

begin 函数返回迭代器是一种常识,且迭代器类型名称往往较长,这种情况应使用 auto:

1
auto i = v.begin();  // OK

又如:

1
2
3
4
5
6
7
8
9
10
struct SomeClass {
struct Sub {
....
};
Sub foo();
};

SomeClass::Sub SomeClass::foo() { // Repeated ‘SomeClass’
....
}

重复的类作用域声明十分繁琐,可用 auto 关键字配合后置返回类型改善:

1
2
3
auto SomeClass::foo() -> Sub {  // OK
....
}

总之,使用 auto 关键字的目的应是提高可读性,而不是单纯地简化代码。

不应使用已过时的关键字

在 C++11 标准中,register 关键字已过时,auto 关键字也不可再作为“存储类说明符(storage class specifier)”。

本规则对 C++ 代码适用,C 代码可不受限制。

示例:

1
2
3
register int a;            // Non-compliant
auto int b; // Non-compliant
int foo(register int x); // Non-compliant

不应使用多余的inline关键词

由 constexpr 关键字限定的函数已经相当于被声明为 inline,不应再重复声明。

示例:

1
2
3
inline constexpr int foo(int n) {  // Non-compliant, ‘inline’ is redundant
return n + 1;
}

应改为:

1
2
3
constexpr int foo(int n) {  // Compliant
return n + 1;
}

另外,在类声明中实现的函数也相当于被声明为 inline,不应重复声明:

1
2
3
4
5
6
7
8
9
10
11
12
class A {
....

public:
inline int foo() { // Non-compliant, ‘inline’ is redundant
return 123;
}

int bar() { // Compliant
return 456;
}
};

extern 关键字不应作用于类成员的声明或定义

extern 关键字作用于类成员的声明或定义是没有意义的,属于语言运用错误。

示例:

1
2
3
4
5
6
7
class A {
void foo();
};

extern void A::foo() { // Non-compliant, invalid ‘extern’
....
}

重写的虚函数应声明为 override 或 final

将重写的虚函数都声明为 override 或 final 有利于提高可读性,并可确保虚函数被有效重写。

示例:

1
2
3
4
5
6
7
8
9
class A {
virtual int foo();
virtual int bar();
};

class B: public A {
int foo(); // Non-compliant
virtual int bar(); // Non-compliant
};

例中 B 重写 A 的 foo 和 bar 这两个虚函数,如果不看 A 的声明,则看不出 B::foo 是虚函数,也看不出 B::bar 是重写的虚函数。

改为如下方式会清晰很多:

1
2
3
4
class B: public A {
int foo() override; // Compliant
int bar() override; // Compliant
};

而且当重写的函数名、参数、返回类型与基类声明不符时,不能通过编译,可及时修正问题。

  • final

    下面是一些 final 关键字的使用示例:

    1、用 final 修饰类,表示该类不能被继承:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Base final {
    public:
    // ...
    };

    // 错误示例,无法继承 final 类
    class Derived : public Base {
    public:
    // ...
    };

    2、用 final 修饰虚函数,表示该虚函数不能被子类重写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class 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
    14
    class Base {
    public:
    void foo() final {
    // ...
    }
    };

    class Derived : public Base {
    public:
    // 错误示例,无法重写 final 函数
    void foo() {
    // ...
    }
    };

override 和 final 关键字不应同时出现在声明中

final 表示不可重写的重写,override 表示可再次重写的重写,这两个关键字不应同时出现在声明中。

示例:

1
2
3
4
class D: public B {
public:
int foo() override final; // Non-compliant, ‘override’ is redundant
};

override 或 final 关键字不应与 virtual 关键字同时出现在声明中

只应在定义新的虚函数时使用 virtual 关键字,重写虚函数应使用 override 或 final 关键字,不应再出现 virtual 关键字。

示例:

1
2
3
4
5
6
7
8
9
10
11
class A {
public:
virtual int foo(); // Compliant, a new virtual function
virtual int bar(); // Compliant, a new virtual function
};

class B: public A {
public:
virtual int foo() final; // Non-compliant, ‘virtual’ is redundant
virtual int bar() override; // Non-compliant, ‘virtual’ is redundant
};

去掉多余的 virtual 关键字使代码更简洁:

1
2
3
4
5
class B: public A {
public:
int foo() final; // Compliant
int bar() override; // Compliant
};

不应将union设为final

在 C++ 语言中,union 不可作为基类,将 union 声明为 final 是没有意义的,属于语言运用错误。

示例:

1
2
3
4
union U final  // Non-compliant, meaningless
{
....
};

未访问 this 指针的成员函数应使用 static 声明

如果未访问 this 指针的成员函数没有被设计为静态成员函数,很可能意味着错误或功能不完整。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
class A {
static int s;

public:
static int bar() { // Compliant
return s--;
}
int foo() { // Non-compliant, missing ‘static’
return s++;
}
....
};

例中 foo 函数只访问了静态数据成员,但在调用时仍会将 this 指针作为参数,这在逻辑上是矛盾的,所以应使用 static 关键字明确声明。

声明和定义内部链接的对象和函数时均应使用 static 关键字

声明和定义内部链接的对象和函数时均应使用 static 关键字,不可使用 extern 关键字,否则极易引起误解。

示例:

1
2
3
4
5
6
extern int a;   // Non-compliant
....
static int a; // ‘a’ is a static object
static int b; // ‘b’ is a static object
....
extern int b; // Non-compliant

例中 a、b 是内部链接的静态对象,在定义的前后不可再用 extern 声明,否则极易与全域对象混淆。

又如:

1
2
3
4
5
6
7
8
9
10
11
int foo(int);          // Bad, missing ‘static’

static int foo(int) {
....
}

static int bar(int);

int bar(int) { // Bad, missing ‘static’
....
}

在声明和定义内部链接的函数时,均应使用 static 关键字,否则也易引起误解。

inline、virtual、static、typedef 等关键字的位置应统一

语言允许 inline、virtual、static、typedef 等关键字出现在类型名称的左侧,也可以出现在其右侧,甚至可以出现在基本类型名称的中间,应对其位置进行统一规范以提高可读性。

本规则对下列 C 或 C++ 关键字有同样的要求:

1
2
3
4
inline、virtual、explicit、
register、static、thread_local、extern、mutable、
friend、typedef、constexpr、
_Alignas、_Atomic、_Noreturn、_Thread_local

这些关键字应统一出现在声明的起始,类型名称的左侧。

对于 const 和 volatile 也需面对类似的问题,参见 ID_badQualifierPosition。

示例:

1
2
3
4
5
6
struct A {
long long typedef LL; // Non-compliant
bool static foo(); // Non-compliant
char friend bar(); // Non-compliant
unsigned int virtual baz(); // Non-compliant
};

例中各种声明均有一定的特殊性,如果声明其特殊性的关键字在类型名称之后,不便于阅读甚至会引起误解。

应改为:

1
2
3
4
5
6
struct A {
typedef long long LL; // Compliant
static bool foo(); // Compliant
friend char bar(); // Compliant
virtual unsigned int baz(); // Compliant
};

未完待续