360代码规范四:global

全局名称应遵循合理的命名方式

全局名称应具有标识性,长度不应过短,否则易与局部名称产生冲突。

本规则是 ID_badName 的特化。

示例:

1
2
3
4
5
6
7
8
// In global scope
const int i = 0; // Non-compliant, name too short
typedef int t; // Non-compliant, name too short
class A { .... }; // Non-compliant, name too short

int foo(int i) {
return i + i; // Confusing
}

名称适用的作用域范围越广,其长度也应该越长,建议全局名称长度不小于 3 个字符。

为代码设定合理的命名空间

命名空间是 C++ 项目的必要组成结构,可有效规避名称冲突等问题。

C++ 代码的顶层作用域应为具名非内联命名空间,命名空间名称应与项目名称相符,且具有标识性。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace NS {
int foo(); // Compliant
int foo(char*); // Compliant
int foo(wchar_t*); // Compliant
}

int foo() { // Non-compliant, it is not ‘int NS::foo()’
....
}

int NS::foo(char*) { // Compliant
....
}

namespace NS {
int foo(wchar_t*) { // Compliant
....
}
}

对于 main 函数和 extern “C” 声明的代码可不受本规则限制,如:

1
2
3
4
5
extern "C" int bar();    // Compliant

int main () { // Compliant
....
}

main函数只应处于全局作用域中

main 函数作为程序的入口,链接器需对其特殊处理,不应受命名空间等作用域的限制。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main() {   // Compliant
....
}

namespace {
int main() { // Non-compliant
....
}
}

namespace NS {
int main() { // Non-compliant
....
}
}

头文件中不应使用using directive

在头文件的全局作用域中使用 using directive 极易造成命名冲突,且影响范围难以控制。

如果代码涉及多个命名空间,而这些命名空间中又有名称相同且功能相似的代码元素时,将造成难以排查的混乱。对于库的头文件,更应该严禁使用全局的 using directive,否则造成对用户命名空间的干扰。

示例:

1
2
3
4
5
6
7
// In a header file
namespace NS {
void foo(short);
}

using namespace NS; // Non-compliant
using namespace std; // Non-compliant

下例展示的问题是头文件不同的包含顺序竟导致同一函数产生了不同的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// In a.h
void foo(char);

namespace ns {
void foo(int);
}

inline void bar() {
foo(0);
}

// In b.h
namespace ns {}
using namespace ns;

// In a.cpp
#include "a.h"
#include "b.h"

void fun1() {
bar(); // ‘bar’ calls ‘foo(char)’
}

// In b.cpp
#include "b.h"
#include "a.h"

void fun2() {
bar(); // ‘bar’ calls ‘foo(int)’
}

头文件 a.h 和 b.h 以不同的顺序被包含,使 bar 函数调用了不同的 foo 函数,导致这种混乱的正是 b.h 中的 using directive。

头文件中不应使用静态声明

头文件中由 static 关键字声明的对象、数组或函数,会在每个包含该头文件的翻译单元或模块中生成副本造成数据冗余,如果将静态数据误用作全局数据也会造成逻辑错误。

类的静态成员不受本规则限制。

示例:

1
2
3
4
5
6
// In a header file
static int i = 0; // Non-compliant

static int foo() { // Non-compliant
return i;
}

在编译每个包含该头文件的源文件时,变量 i 和函数 foo 都会生成不必要的副本。

在头文件中实现的内联或模板函数中,也不应使用静态声明,如:

1
2
3
4
5
// In a header file
inline void bar() {
static Type obj; // Non-compliant
....
}

如果该头文件被不同的模块(so、dll、exe)包含,obj 对象会生成不同的副本,很可能造成逻辑错误。

另外,由 const 或 constexpr 关键字限定的常量也具有静态数据的特性,在头文件中定义常量也面对这种问题,基本类型的常量经过编译优化可以不占用存储空间(有取地址操作的除外),而对于非基本类型的常量对象或数组也不应在头文件中定义,建议采用单件模式,将其数据定义在 cpp 等源文件中,在头文件中定义访问这些数据的接口,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// In arr.h
using Arr = int[256];
const Arr& getArr();

// In arr.cpp
#include "arr.h"

const Arr& getArr() {
static Arr a = {
1, 2, 3, ....
};
return a;
}

在需要用到常量数组的地方调用 getArr 函数,即可获取该数组的引用,没有任何重复的数据产生,并可保证数组在使用之前被有效初始化。

头文件中不应定义匿名命名空间

在头文件中定义匿名命名空间相当于在头文件中定义静态数据,头文件被多个源文件包含时会造成数据冗余。

可参见 ID_staticInHeader 的进一步讨论。

示例:

1
2
3
4
// In a header file
namespace { // Non-compliant
void foo();
}

匿名命名空间中不应使用静态声明

匿名命名空间中的元素已具有内部链接性(internal linkage),不应再用 static 关键字限定。

示例:

1
2
3
4
5
6
namespace {
static int i = 0; // Non-compliant
static int foo() { // Non-compliant
return i++;
}
}

例中 static 关键字是多余的。

应改为:

1
2
3
4
5
6
namespace {
int i = 0; // Compliant
int foo() { // Compliant
return i++;
}
}

全局对象的初始化不可依赖未初始化的对象

全局对象的初始化或构造过程不可依赖在其他源文件中定义的全局对象,也不可依赖在其后面定义的对象。

在不同源文件中定义的全局对象,以及类的静态成员对象,其初始化顺序是不确定的,在同一源文件中定义的对象,排在前面的会先于后面的初始化。为避免产生问题,建议只使用基本类型的常量作为全局对象,且尽量不要使用 extern 关键字。

示例:

1
2
extern int i;   // Defined in other translate unit
int j = i; // Non-compliant

例中 i 是在其他源文件中定义的对象,j 初始化时无法保证 i 已被正确初始化。

又如:

1
2
3
4
5
6
int foo() {
return 1;
}
extern int x; // Defined after ‘y’
int y = x; // Non-compliant, unspecified
int x = foo();

例中 x 在 y 的后面定义,y 会先于 x 初始化,y 的值是 0 还是 foo 函数的返回值在标准中是未声明的。