360代码规范三:precompile

Include

include指令应符合标准格式

#include 后只应为 < 头文件路径 > 或 “ 头文件路径 “,否则会导致标准未定义的行为。

示例:

1
2
3
4
5
6
7
#include <string.h>         // Compliant
#include "string.h" // Compliant

#define HEADER "string.h"
#include HEADER // Compliant

#include stdlib.h // Non-compliant, undefined behavior

例中对 string.h 的引用符合标准,而对 stdlib.h 的引用会导致标准未定义的行为。

注意,由引号标识的头文件路径并非字符串常量,不应对其使用字符串常量的特性,如:

1
#include "stdlib" ".h"      // Non-compliant, implementation defined

是否会将引号中的内容连接成一个路径是由实现定义的,这种代码是不可移植的。

另外,如下形式的代码也是不符合标准的:

1
2
3
4
#include L"foo"             // Non-compliant
#include u"bar" // Non-compliant
#include U"baz" // Non-compliant
#include R"(..\foo\bar)" // Non-compliant

include指令中禁用不合规的字符

字母、数字、下划线、点号之外的字符可能与文件系统存在冲突,也可能导致标准未定义的行为,不应出现在头文件和相关目录名称中。

示例:

1
2
3
4
5
6
7
#include <"foo">        // Non-compliant
#include <foo*> // Non-compliant
#include <foo'bar> // Non-compliant

#include <foo> // Compliant
#include <foo.h> // Compliant
#include <foo_bar> // Compliant

可以用 / 作为路径分隔符,但不应出现 // 或 /*, 如:

1
2
#include <foo//bar.h>   // Non-Compliant, undefined behavior
#include <foo/*bar.h> // Non-Compliant, undefined behavior

另外,某些平台的文件路径不区分大小写,建议在头文件名称中只使用小写字母以提高可移植性。

include指令中不应使用反斜杠

如果在 include 指令中使用反斜杠,程序的行为在 C 和 C++03 标准中是未定义的,在 C++11 标准中是由实现定义的。

示例:

1
2
3
4
#include <foo\bar.h>     // Non-compliant
#include "foo\\bar.h" // Non-compliant

#include <foo/bar.h> // Compliant

在有可移植性要求的代码中应避免使用反斜杠。

include指令中不应该使用绝对路径

绝对路径使代码过分依赖编译环境,意味着项目的编译设置不完善,应使用相对路径。

示例:

1
2
#include "C:\\foo\\bar.h"   // Non-compliant
#include "/foo/bar.h" // Non-compliant

禁用不合规的头文件

已过时的、无意义的或有不良副作用的头文件应禁用。

示例:

1
2
3
4
5
6
7
8
9
10
#include <tgmath.h>   // Non-compliant
#include <setjmp.h> // Non-compliant

#include <iso646.h> // Non-compliant in C++
#include <stdbool.h> // Non-compliant in C++
#include <ciso646> // Non-compliant in C++
#include <cstdbool> // Non-compliant in C++
#include <ctgmath> // Non-compliant in C++
#include <ccomplex> // Non-compliant in C++
#include <cstdalign> // Non-compliant in C++

tgmath.h 和 ctgmath 会使用语言标准之外的技术实现某种重载效果,而且其中的部分函数名称会干扰其他标准库中的名称,setjmp.h 和 csetjmp 则包含危险的过程间跳转函数。

iso646.h、stdalign.h、stdbool.h 以及 ciso646、cstdalign、cstdbool 等头文件对 C++ 语言没有意义,ccomplex、cstdalign、cstdbool、ctgmath 等头文件在 C++17 标准中已过时,在 C++ 代码中不应使用这些头文件。

stdio.h、signal.h、time.h、fenv.h 等头文件含有较多标准未声明或由实现定义的内容,对有高可靠性要求的软件系统也不建议使用。

审计工具不妨通过配置设定不合规头文件的名称,如:

1
2
3
[ID_forbiddenHeader]
inC=tgmath.h|setjmp.h
inCpp=tgmath.h|ctgmath|setjmp.h|csetjmp

表示对 C 代码将 tgmath.h、setjmp.h 设为不合规,对 C++ 代码将 tgmath.h、ctgmath、setjmp.h、csetjmp 设为不合规。

C++代码不应引用C头文件

为了与 C 语言兼容,C++ 标准库也会提供 C 头文件,但在这种 C 头文件在 C++ 标准中是已过时的。

C 标准头文件均有对应的 C++ 版本,C++ 版本提供了更适合 C++ 代码的命名空间、模板以及函数重载等功能。C 标准不在 C++ 标准之内,在 C++ 代码中不建议使用 C 标准库的功能,如果确有必要,应使用 C++ 版本的头文件。

本规则是 ID_forbiddenHeader 的特化。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <assert.h>    // Non-compliant, use <cassert>
#include <ctype.h> // Non-compliant, use <cctype>
#include <errno.h> // Non-compliant, use <cerrno>
#include <float.h> // Non-compliant, use <cfloat>
#include <limits.h> // Non-compliant, use <climits>
#include <locale.h> // Non-compliant, use <clocale>
#include <math.h> // Non-compliant, use <cmath>
#include <setjmp.h> // Non-compliant, use <csetjmp>
#include <signal.h> // Non-compliant, use <csignal>
#include <stdarg.h> // Non-compliant, use <cstdarg>
#include <stddef.h> // Non-compliant, use <cstddef>
#include <stdio.h> // Non-compliant, use <cstdio>
#include <stdlib.h> // Non-compliant, use <cstdlib>
#include <string.h> // Non-compliant, use <cstring>
#include <time.h> // Non-compliant, use <ctime>
#include <wchar.h> // Non-compliant, use <cwchar>
#include <wctype.h> // Non-compliant, use <cwctype>

Macro-definition

宏应遵循合理的命名方式

宏的名称应采用全大写字母的形式,非宏名称则应包含小写字母。

宏用于文本处理,不受语言规则限制,易被误用,在命名方式上将其与普通代码分开可引起使用者或维护者的注意,有助于规避错误。

本规则是 ID_badName 的特化,宏名称同样受 ID_badName 的约束。

示例:

1
2
#define word_size 8   // Non-compliant, like a normal variable
#define WORD_SIZE 8 // Compliant

不可定义具有保留意义的宏名称

重新定义已有特殊用途的名称会导致标准未定义的行为,也会使代码陷入难以维护的境地。

标准库、编译环境中的名称以及关键字不应重新定义。

C++ 标准指明不可重新定义的宏有:

1
2
3
4
__cplusplus、__TIME__、__DATE__、__FILE__、__ LINE__、
__STDC__、__STDC_HOSTED__、__STDCPP_THREADS__、
__STDC_MB_MIGHT_NEQ_WC__、__STDC_VERSION__、
__STDC_ISO_10646__、__STDCPP_STRICT_POINTER_SAFETY__

以下划线开头的名称用于表示标准库或编译环境的保留名称,自定义名称不应以下划线开头。

示例:

1
2
3
4
#define _WIN64   0      // Non-compliant
#define __GNUC__ 1 // Non-compliant
#define __STDC__ 1 // Non-compliant, undefined behavior
#define __cplusplus 0 // Non-compliant, undefined behavior

标识平台或编译环境的宏不可在代码中写死。

1
2
#define defined            // Non-compliant, undefined behavior
#define new new(nothrow) // Non-compliant

不可重定义关键字。

1
2
3
#define NDEBUG 0    // Non-compliant
#define errno 0 // Non-compliant
#define assert(x) // Non-compliant

编译优化相关的宏不可在代码中写死,标准库中的名称不应被重新定义。

不可取消定义具有保留意义的宏名称

取消定义已有特殊用途的宏会导致标准未定义的行为,也会使代码陷入难以维护的境地。

标准库、编译环境中的宏不可被取消定义。

示例:

1
2
3
4
#undef __LINE__      // Non-compliant
#undef __cplusplus // Non-compliant
#undef _WIN64 // Non-compliant
#undef NDEBUG // Non-compliant

只应在全局作用域中定义宏

宏不受作用域限制,在非全局作用域中定义宏易引起误解。

示例:

1
2
3
4
void foo(void) {
#define M 123 // Non-compliant, defined in a function scope
....
}

例中宏 M 在函数中定义,但其作用范围却是全局的。

如果宏与某作用域密切相关,在该作用域内定义宏,使用后再取消定义是一种惯用方式,如:

1
2
3
4
5
void foo(void) {
#define M 123 // Let it go?
....
#undef M
}

审计工具不妨通过配置决定是否放过这种情况。

避免宏被取消定义

宏不受作用域限制,不应被取消定义,否则会失去确定性,使代码难以维护。

示例:

1
2
3
4
5
6
// In a.h
#define M 1

// In b.h
#undef M // Non-compliant
#define M 0 // Redefined, bad

在一个文件中定义了宏 M,在另一个文件取消并重定义了 M,使同一个全局名称产生两种不同的意义,严重降低了可维护性。

有时取消定义已使用完毕的内部宏可避免对外部产生不良影响,具有一定积极作用,但宏的定义和取消应在同一文件的同一作用域中完成,相关示例可参见 ID_macro_inBlock。

Macro usage

宏的实参不应有副作用

当宏参数有“副作用(side effect))”时,如果宏定义中没有或多次引用到该参数,会导致意料之外的错误。

示例:

1
2
3
4
5
6
7
8
9
10
#define I(a)
#define M(a) ((a) + (a))

int foo(int& a) {
return M(++a); // Non-compliant, returns ‘((++a) + (++a))’
}

void bar(int& a) {
I(a--); // Non-compliant, does nothing
}

例中 M 和 I 看起来像是函数调用,而展开后的结果却在意料之外。

宏的实参个数不可小于形参个数

宏的实参个数小于形参个数是不符合 C/C++ 标准的,参数个数不一致必然意味着某种错误,然而在某些编译环境下却可以通过编译。

示例:

1
2
3
4
5
#define M(a, b, c)  a ## b ## c

const char* foo() {
return M("x", "y"); // Non-compliant
}

早期标准(ISO 9899:1990)对这种情况没有明确定义,后续标准对其进行了约束,但 MSVC 等编译器至今仍不把这种问题视作编译错误,需要特别注意。

宏的实参个数不可大于形参个数

宏的实参个数大于形参个数是不符合 C/C++ 标准的,多余的宏参数是没有意义的,然而在某些编译环境下却可以通过编译。

示例:

1
2
3
4
5
#define M(a, b, c)  a ## b ## c

const char* foo() {
return M("a", "b", "c", "d"); // Non-compliant
}

例外:

1
2
3
4
5
#define MSG(fmt, ...) printf(fmt, __VA_ARGS__)

int main() {
MSG("%d %d\n", 1, 2); // Compliant
}

可变宏参数列表可不受本规则约束。

va_start 或 va_copy 应配合 va_end 使用

可变参数列表相关的 va_start 或 va_copy 和 va_end 应在同一函数中使用,否则会导致标准未定义的行为。

示例:

1
2
3
4
5
6
7
8
9
int foo(int n, ...) {
va_list ap;
va_start(ap, n);
int sum = 0;
for (int i = 0; i < n; i++) {
sum += va_arg(ap, int);
}
return sum; // Non-compliant, missing ‘va_end(ap);’
}

应在函数返回前使用 va_end。

va_arg的类型参数应符合要求

对于 stdarg.h 中的宏 va_arg(ap, type),其类型参数 type 在

对于宏 va_arg(ap, type) 的类型参数 type,下列情况会导致标准未定义的行为:

  • type 后加 * 号不能表示指针类型
  • 与“默认参数提升”后的类型不兼容
  • 与可变参数列表中对应的实参类型不兼容,或没有对应的实参

以下类型不可作为 av_arg 的参数:

1
2
3
4
5
bool、_Bool、
char、signed char、unsigned char、char16_t、
float、
short、unsigned short、signed short、
short int、signed short int、unsigned short int

这些类型的参数在传入可变参数列表时,会被提升为 int、unsigned int、double 等类型,va_arg 如果再按提升前的类型解析参数的值就会产生错误,参见“默认参数提升(default argument promotion)”机制。

另外,C++ 代码中非 POD 类型也不可作为 va_arg 的参数,参见 ID_nonPODVariadicArgument。

示例:

1
2
3
4
5
6
7
8
9
void foo(int n, ...) {
va_list ap;
va_start(ap, n);
for (int i = 0; i < n; i++) {
char c = va_arg(ap, char); // Non-compliant
....
}
va_end(ap);
}

例中 va_arg 的类型参数为 char 是不符合要求的。

应改为:

1
2
3
4
for (int i = 0; i < n; i++) {
char c = (char)va_arg(ap, int); // Compliant
....
}

在 C++ 代码中不应使用宏 offsetof

宏 offsetof 很难适用于具有 C++ 特性的类,在 C++ 代码中不应使用。

如果 offsetof 用于:

  • 非“standard-layout”类型
  • 计算静态成员或成员函数的偏移量

会导致标准未定义的行为。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct A {
int i;
virtual int f();
};

int foo() {
return offsetof(A, i); // Non-compliant, undefined behavior
}

struct B {
static int i;
int f();
};

int bar() {
return offsetof(B, i); // Non-compliant, undefined behavior
}

int baz() {
return offsetof(B, f); // Non-compliant, undefined behavior
}

Directive

头文件不应缺少守卫

以 .h 或 .hpp 为扩展名的头文件应包含头文件守卫。

示例:

1
2
3
4
5
// Header file foo.h
#ifndef LIBRARY_FOO_H
#define LIBRARY_FOO_H
....
#endif

例中 foo.h 是“Library”模块中的头文件,宏 LIBRARY_FOO_H 即可作为它的守卫,保证头文件被重复引入也不会出现问题,守卫名称不可有重复,建议守卫名称遵循“模块名_文件名”的形式。

#pragma once 指令也可作为头文件守卫,但并不是 C/C++ 的标准方式,只是多数编译器均有支持。这种方式由编译器维护一个列表,引入头文件时,如果发现文件中有 #pragma once 指令就将文件路径加入列表,当这个文件再次被 include 时便不会加载,而宏守卫的方式仍然要对文件进行预编译,所以 #pragma once 方式在编译效率上会更高一些。

宏守卫用宏名区分头文件,所以不能有重复。宏的引入可以使相关设定更灵活,比如声明头文件之间的依赖或排斥关系,如果 bar.h 依赖 foo.h,在 #include “bar.h” 之前必须 #include “foo.h”,可在 bar.h 中设置:

1
2
3
4
// Header file bar.h
#ifndef LIBRARY_FOO_H
#error foo.h should be included first
#endif

这样如果不满足条件无法通过编译。

本规则建议使用宏守卫的方式,但 #pragma once 方法也是惯用写法,不妨通过配置项决定其是否合规。

不应出现非标准格式的预编译指令

非标准格式的预编译指令往往意味着错误,也会导致标准未定义的行为。

需注意:

  • defined 只应作用于宏名称或括号括起来的宏名称
  • defined 不应出现在宏定义中
  • #if、#elif 之后应为正确的常量表达式
  • #ifdef、#ifndef 之后只应为宏名称
  • #else、#endif 之后应直接换行
  • #line 之后应接整数常量,或整数常量和文件名称
  • #line 指定的行号应在有效范围内
  • #line 不应出现在非自动生成的代码中

示例:

1
2
3
4
5
6
7
8
9
#if defined M            // Compliant
#if defined(M) // Compliant
#if defined(M == 0) // Non-compliant, undefined behavior

#define DEFINED defined // Non-compliant
#if DEFINED M // Undefined behavior

#line 0 // Non-compliant, invalid line number
#line 4294967295 // Non-compliant, line number too large

例中作用于比较表达式的 defined 和 #if 条件中由宏展开产生的 defined 均会导致未定义的行为,由 #line 指定的行号应大于 0 且小于 2147483648(按 C++03 标准则应小于 32768),否则也会导致未定义的行为。

又如:

1
2
3
4
5
6
7
8
9
10
11
#define M 2

int foo() {
int x = 0;
#ifdef M
x = M;
#elif // Non-compliant, use ‘#else’ instead
x = 1;
#endif M // Non-compliant, remove ‘M’
return x;
}

这种代码是不符合标准的,但可被某些编译器接受,应避免。

避免使用 pragma 指令

应避免使用由实现定义的 pragma 指令以提高可移植性。

示例:

1
#pragma once   // Non-compliant, use macro header guards instead

应使用标准方法代替 pragma 指令,如果难以代替,相关 pragma 指令应备以文档说明。

非自动生成的代码中不应出现 line 指令

在非自动生成的代码中没有必要使用 line 指令,否则会干扰编译器的输出,使问题难以定位。

示例:

1
2
#line 123           // Non-compliant
#line 456 "foo.c" // Non-compliant

宏的参数列表中不应出现预编译指令

如果预编译指令出现在宏的参数列表中,会导致标准未定义的行为。

示例:

1
2
3
4
5
6
7
8
9
#define PRINT(s) printf(#s)

PRINT(
#ifdef MAC // Non-compliant, undefined behavior
rabbit
#else // Non-compliant
hamster
#endif // Non-compliant
);

可能会打印出 hamster,也可能是 #ifdef MAC rabbit #else hamster #endif 这种怪异的结果。

条件编译代码块应在同一文件中

#if、#ifdef 与对应的 #else、#elif、#endif 应在同一文件中,否则会增加代码的维护成本。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
// a.h
#ifdef M // Non-compliant
....

// b.h
#else // Non-compliant
....

// c.h
#include "a.h"
#include "b.h"
#endif // Non-compliant

示例代码将 #ifdef、#else、#endif 分成了三个文件,使这些文件的依赖关系变得复杂,也使单个文件失去了可读性。

对编译警告的屏蔽应慎重

编译器一般允许使用预编译指令屏蔽某些编译警告,但对于反映风险或安全问题的警告不应屏蔽。

示例:

1
2
3
4
5
#ifdef _MSC_VER
#pragma warning(disable: 4172) // Non-compliant
#elif defined __GNUC__
#pragma GCC diagnostic ignored "-Wreturn-local-addr" // Non-compliant
#endif

示例代码屏蔽了 Visual Studio C4172 和 GCC -Wreturn-local-addr 对应的警告,当局部变量的地址被返回时编译器不会给出警告,但这种警告是不应该被屏蔽的,详见 ID_localAddressFlowOut。

本规则集合提到的部分问题编译器也可以给出警告,这种警告均不应被屏蔽。

在高级别的警告设置下编译

编译器一般允许设定编译警告的级别,级别越高关注的问题就越多,也可以将警告设为错误,当有警告产生时停止编译,建议代码在高级别的警告设置下编译。

应避免代码中出现 #pragma warning(default:…) 等指令,这种指令将警告级别设为默认,可能与整个项目的设置不一致,如果一定要使用,应改用 #pragma warning(pop) 方式。

示例:

1
2
3
#pragma warning(disable:4706)
#include "somecode"
#pragma warning(default:4706) // Non-compliant

示例代码在导入某些代码之前将代号为 4706 的警告屏蔽,之后又将其设为默认级别,首先要关注 4706 是否应该被屏蔽,还要关注如果将其设为默认是否与整个项目的设置有冲突。

应改为:

1
2
3
4
#pragma warning(push)
#pragma warning(disable:4706)
#include "somecode"
#pragma warning(pop) // Compliant

改用这种方式之后不必再关注是否与整个项目的设置有冲突了。

Comment

关注TODO、FIXME等特殊注释

TODO、FIXME、XXX、BUG 等特殊注释表示代码中存在问题,这种问题不应被遗忘,应有计划地予以解决。

及时记录问题是一种好习惯,而且最好有署名和日期。

示例:

1
2
3
4
5
6
7
8
9
10
11
void foo() {
/*
* Some plans... // Bad, easy to forget
*/
}

void foo() {
/* TODO:
* Some plans... -- my name, date // Good
*/
}

审计工具不妨定期搜索这些关键词对应的注释,以供相关人员核对问题解决情况。

注释不可嵌套

嵌套的 /*…*/ 注释不符合标准,/* 与 */ 之间不应出现 /*,某些编译器可以接受嵌套,但不具备可移植性。

示例:

1
2
3
4
5
/*                         // #1
/* // #2, Non-compliant
nested comments
*/ // #3
*/ // #4, Non-compliant

根据标准,#1 处的 /* 与 #3 处的 */ 匹配,而 #4 处的 */ 处于失配状态。

注释应出现在合理的位置

注释应出现在段落的前后或行尾,不应出现在行首或中间,否则干扰阅读,甚至会导致标准未定义的行为。

示例:

1
2
3
4
5
6
7
#/*comment*/include "foo.h"         // Non-compliant
#include <bar.h /*comment*/> // Non-compliant, undefined behavior

/*comment*/ int main() // Non-compliant
{
return a + b /*comment*/ + c; // Non-compliant
}

应改为:

1
2
3
4
5
6
7
8
9
10
#include "foo.h"    // comment      // Compliant
#include <bar.h> // comment // Compliant

/*
* comment // Compliant
*/
int main()
{
return a + b + c; // comment // Compliant
}

例外:

1
2
3
4
5
void foo(int i = 0);                // Declaration

void foo(int i /*= 0*/) { // Let it go
....
}

如果参数有默认值,在函数实现中参数声明的结尾可用注释说明,不受本规则限制。

Other

非空源文件应以换行符结尾

如果非空源文件未以换行符结尾,或以换行符结尾但换行符之前是反斜杠,在 C 和 C++03 标准中会导致未定义的行为。

一般情况下 IDE 或编辑器会保证源文件以空行结尾,而且 C++11 规定编译器应补全所需的空行,但为了提高兼容性,并便于各种相关工具的使用,所有与代码相关的文本文件均应以有效的换行符结尾。

除转义字符、宏定义之外不应使用反斜杠

反斜杠可用于标识转义字符,也可用于实现“伪换行”,即代码换行显示但在语法上并没有换行,一般用于宏定义,除此之外不应再使用反斜杠,否则没有实际意义,也会造成混乱。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define M(x, y) if(x) {\    // Compliant
foo(y);\ // Compliant
}

void foo() {
if (condition1 \ // Non-compliant, meaningless
|| condition2) {
}
}

int a\ // Non-compliant, odd usage
b\
c = 123;

void bar() {
// comment \ // Non-compliant, The next line is also commented out
do_something();
}

如果“universal character name”被反斜杠截断会导致标准未定义的行为,如:

1
2
const char* s = "\u4e\      // Non-compliant, undefined behavior
2d";

应去掉反斜杠:

1
const char* s = "\u4e2d";   // Compliant