360代码规范三:precompile
360代码规范三:precompile
Hoshea ZhangInclude
include指令应符合标准格式
#include 后只应为 < 头文件路径 > 或 “ 头文件路径 “,否则会导致标准未定义的行为。
示例:
1 | #include <string.h> // Compliant |
例中对 string.h 的引用符合标准,而对 stdlib.h 的引用会导致标准未定义的行为。
注意,由引号标识的头文件路径并非字符串常量,不应对其使用字符串常量的特性,如:
1 | #include "stdlib" ".h" // Non-compliant, implementation defined |
是否会将引号中的内容连接成一个路径是由实现定义的,这种代码是不可移植的。
另外,如下形式的代码也是不符合标准的:
1 | #include L"foo" // Non-compliant |
include指令中禁用不合规的字符
字母、数字、下划线、点号之外的字符可能与文件系统存在冲突,也可能导致标准未定义的行为,不应出现在头文件和相关目录名称中。
示例:
1 | #include <"foo"> // Non-compliant |
可以用 / 作为路径分隔符,但不应出现 // 或 /*, 如:
1 | #include <foo//bar.h> // Non-Compliant, undefined behavior |
另外,某些平台的文件路径不区分大小写,建议在头文件名称中只使用小写字母以提高可移植性。
include指令中不应使用反斜杠
如果在 include 指令中使用反斜杠,程序的行为在 C 和 C++03 标准中是未定义的,在 C++11 标准中是由实现定义的。
示例:
1 | #include <foo\bar.h> // Non-compliant |
在有可移植性要求的代码中应避免使用反斜杠。
include指令中不应该使用绝对路径
绝对路径使代码过分依赖编译环境,意味着项目的编译设置不完善,应使用相对路径。
示例:
1 | #include "C:\\foo\\bar.h" // Non-compliant |
禁用不合规的头文件
已过时的、无意义的或有不良副作用的头文件应禁用。
示例:
1 | #include <tgmath.h> // Non-compliant |
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 | [ID_forbiddenHeader] |
表示对 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 | #include <assert.h> // Non-compliant, use <cassert> |
Macro-definition
宏应遵循合理的命名方式
宏的名称应采用全大写字母的形式,非宏名称则应包含小写字母。
宏用于文本处理,不受语言规则限制,易被误用,在命名方式上将其与普通代码分开可引起使用者或维护者的注意,有助于规避错误。
本规则是 ID_badName 的特化,宏名称同样受 ID_badName 的约束。
示例:
1 | #define word_size 8 // Non-compliant, like a normal variable |
不可定义具有保留意义的宏名称
重新定义已有特殊用途的名称会导致标准未定义的行为,也会使代码陷入难以维护的境地。
标准库、编译环境中的名称以及关键字不应重新定义。
C++ 标准指明不可重新定义的宏有:
1 | __cplusplus、__TIME__、__DATE__、__FILE__、__ LINE__、 |
以下划线开头的名称用于表示标准库或编译环境的保留名称,自定义名称不应以下划线开头。
示例:
1 | #define _WIN64 0 // Non-compliant |
标识平台或编译环境的宏不可在代码中写死。
1 | #define defined // Non-compliant, undefined behavior |
不可重定义关键字。
1 | #define NDEBUG 0 // Non-compliant |
编译优化相关的宏不可在代码中写死,标准库中的名称不应被重新定义。
不可取消定义具有保留意义的宏名称
取消定义已有特殊用途的宏会导致标准未定义的行为,也会使代码陷入难以维护的境地。
标准库、编译环境中的宏不可被取消定义。
示例:
1 | #undef __LINE__ // Non-compliant |
只应在全局作用域中定义宏
宏不受作用域限制,在非全局作用域中定义宏易引起误解。
示例:
1 | void foo(void) { |
例中宏 M 在函数中定义,但其作用范围却是全局的。
如果宏与某作用域密切相关,在该作用域内定义宏,使用后再取消定义是一种惯用方式,如:
1 | void foo(void) { |
审计工具不妨通过配置决定是否放过这种情况。
避免宏被取消定义
宏不受作用域限制,不应被取消定义,否则会失去确定性,使代码难以维护。
示例:
1 | // In a.h |
在一个文件中定义了宏 M,在另一个文件取消并重定义了 M,使同一个全局名称产生两种不同的意义,严重降低了可维护性。
有时取消定义已使用完毕的内部宏可避免对外部产生不良影响,具有一定积极作用,但宏的定义和取消应在同一文件的同一作用域中完成,相关示例可参见 ID_macro_inBlock。
Macro usage
宏的实参不应有副作用
当宏参数有“副作用(side effect))”时,如果宏定义中没有或多次引用到该参数,会导致意料之外的错误。
示例:
1 | #define I(a) |
例中 M 和 I 看起来像是函数调用,而展开后的结果却在意料之外。
宏的实参个数不可小于形参个数
宏的实参个数小于形参个数是不符合 C/C++ 标准的,参数个数不一致必然意味着某种错误,然而在某些编译环境下却可以通过编译。
示例:
1 | #define M(a, b, c) a ## b ## c |
早期标准(ISO 9899:1990)对这种情况没有明确定义,后续标准对其进行了约束,但 MSVC 等编译器至今仍不把这种问题视作编译错误,需要特别注意。
宏的实参个数不可大于形参个数
宏的实参个数大于形参个数是不符合 C/C++ 标准的,多余的宏参数是没有意义的,然而在某些编译环境下却可以通过编译。
示例:
1 | #define M(a, b, c) a ## b ## c |
例外:
1 | #define MSG(fmt, ...) printf(fmt, __VA_ARGS__) |
可变宏参数列表可不受本规则约束。
va_start 或 va_copy 应配合 va_end 使用
可变参数列表相关的 va_start 或 va_copy 和 va_end 应在同一函数中使用,否则会导致标准未定义的行为。
示例:
1 | int foo(int n, ...) { |
应在函数返回前使用 va_end。
va_arg的类型参数应符合要求
对于 stdarg.h 中的宏 va_arg(ap, type),其类型参数 type 在
对于宏 va_arg(ap, type) 的类型参数 type,下列情况会导致标准未定义的行为:
- type 后加 * 号不能表示指针类型
- 与“默认参数提升”后的类型不兼容
- 与可变参数列表中对应的实参类型不兼容,或没有对应的实参
以下类型不可作为 av_arg 的参数:
1 | bool、_Bool、 |
这些类型的参数在传入可变参数列表时,会被提升为 int、unsigned int、double 等类型,va_arg 如果再按提升前的类型解析参数的值就会产生错误,参见“默认参数提升(default argument promotion)”机制。
另外,C++ 代码中非 POD 类型也不可作为 va_arg 的参数,参见 ID_nonPODVariadicArgument。
示例:
1 | void foo(int n, ...) { |
例中 va_arg 的类型参数为 char 是不符合要求的。
应改为:
1 | for (int i = 0; i < n; i++) { |
在 C++ 代码中不应使用宏 offsetof
宏 offsetof 很难适用于具有 C++ 特性的类,在 C++ 代码中不应使用。
如果 offsetof 用于:
- 非“standard-layout”类型
- 计算静态成员或成员函数的偏移量
会导致标准未定义的行为。
示例:
1 | struct A { |
Directive
头文件不应缺少守卫
以 .h 或 .hpp 为扩展名的头文件应包含头文件守卫。
示例:
1 | // Header file foo.h |
例中 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 | // Header file bar.h |
这样如果不满足条件无法通过编译。
本规则建议使用宏守卫的方式,但 #pragma once 方法也是惯用写法,不妨通过配置项决定其是否合规。
不应出现非标准格式的预编译指令
非标准格式的预编译指令往往意味着错误,也会导致标准未定义的行为。
需注意:
- defined 只应作用于宏名称或括号括起来的宏名称
- defined 不应出现在宏定义中
- #if、#elif 之后应为正确的常量表达式
- #ifdef、#ifndef 之后只应为宏名称
- #else、#endif 之后应直接换行
- #line 之后应接整数常量,或整数常量和文件名称
- #line 指定的行号应在有效范围内
- #line 不应出现在非自动生成的代码中
示例:
1 | #if defined M // Compliant |
例中作用于比较表达式的 defined 和 #if 条件中由宏展开产生的 defined 均会导致未定义的行为,由 #line 指定的行号应大于 0 且小于 2147483648(按 C++03 标准则应小于 32768),否则也会导致未定义的行为。
又如:
1 | #define M 2 |
这种代码是不符合标准的,但可被某些编译器接受,应避免。
避免使用 pragma 指令
应避免使用由实现定义的 pragma 指令以提高可移植性。
示例:
1 | #pragma once // Non-compliant, use macro header guards instead |
应使用标准方法代替 pragma 指令,如果难以代替,相关 pragma 指令应备以文档说明。
非自动生成的代码中不应出现 line 指令
在非自动生成的代码中没有必要使用 line 指令,否则会干扰编译器的输出,使问题难以定位。
示例:
1 | #line 123 // Non-compliant |
宏的参数列表中不应出现预编译指令
如果预编译指令出现在宏的参数列表中,会导致标准未定义的行为。
示例:
1 | #define PRINT(s) printf(#s) |
可能会打印出 hamster,也可能是 #ifdef MAC rabbit #else hamster #endif 这种怪异的结果。
条件编译代码块应在同一文件中
#if、#ifdef 与对应的 #else、#elif、#endif 应在同一文件中,否则会增加代码的维护成本。
示例:
1 | // a.h |
示例代码将 #ifdef、#else、#endif 分成了三个文件,使这些文件的依赖关系变得复杂,也使单个文件失去了可读性。
对编译警告的屏蔽应慎重
编译器一般允许使用预编译指令屏蔽某些编译警告,但对于反映风险或安全问题的警告不应屏蔽。
示例:
1 | #ifdef _MSC_VER |
示例代码屏蔽了 Visual Studio C4172 和 GCC -Wreturn-local-addr 对应的警告,当局部变量的地址被返回时编译器不会给出警告,但这种警告是不应该被屏蔽的,详见 ID_localAddressFlowOut。
本规则集合提到的部分问题编译器也可以给出警告,这种警告均不应被屏蔽。
在高级别的警告设置下编译
编译器一般允许设定编译警告的级别,级别越高关注的问题就越多,也可以将警告设为错误,当有警告产生时停止编译,建议代码在高级别的警告设置下编译。
应避免代码中出现 #pragma warning(default:…) 等指令,这种指令将警告级别设为默认,可能与整个项目的设置不一致,如果一定要使用,应改用 #pragma warning(pop) 方式。
示例:
1 | #pragma warning(disable:4706) |
示例代码在导入某些代码之前将代号为 4706 的警告屏蔽,之后又将其设为默认级别,首先要关注 4706 是否应该被屏蔽,还要关注如果将其设为默认是否与整个项目的设置有冲突。
应改为:
1 | #pragma warning(push) |
改用这种方式之后不必再关注是否与整个项目的设置有冲突了。
Comment
关注TODO、FIXME等特殊注释
TODO、FIXME、XXX、BUG 等特殊注释表示代码中存在问题,这种问题不应被遗忘,应有计划地予以解决。
及时记录问题是一种好习惯,而且最好有署名和日期。
示例:
1 | void foo() { |
审计工具不妨定期搜索这些关键词对应的注释,以供相关人员核对问题解决情况。
注释不可嵌套
嵌套的 /*…*/ 注释不符合标准,/* 与 */ 之间不应出现 /*,某些编译器可以接受嵌套,但不具备可移植性。
示例:
1 | /* // #1 |
根据标准,#1
处的 /* 与 #3
处的 */ 匹配,而 #4
处的 */ 处于失配状态。
注释应出现在合理的位置
注释应出现在段落的前后或行尾,不应出现在行首或中间,否则干扰阅读,甚至会导致标准未定义的行为。
示例:
1 | #/*comment*/include "foo.h" // Non-compliant |
应改为:
1 | #include "foo.h" // comment // Compliant |
例外:
1 | void foo(int i = 0); // Declaration |
如果参数有默认值,在函数实现中参数声明的结尾可用注释说明,不受本规则限制。
Other
非空源文件应以换行符结尾
如果非空源文件未以换行符结尾,或以换行符结尾但换行符之前是反斜杠,在 C 和 C++03 标准中会导致未定义的行为。
一般情况下 IDE 或编辑器会保证源文件以空行结尾,而且 C++11 规定编译器应补全所需的空行,但为了提高兼容性,并便于各种相关工具的使用,所有与代码相关的文本文件均应以有效的换行符结尾。
除转义字符、宏定义之外不应使用反斜杠
反斜杠可用于标识转义字符,也可用于实现“伪换行”,即代码换行显示但在语法上并没有换行,一般用于宏定义,除此之外不应再使用反斜杠,否则没有实际意义,也会造成混乱。
示例:
1 | #define M(x, y) if(x) {\ // Compliant |
如果“universal character name”被反斜杠截断会导致标准未定义的行为,如:
1 | const char* s = "\u4e\ // Non-compliant, undefined behavior |
应去掉反斜杠:
1 | const char* s = "\u4e2d"; // Compliant |