在《华为C++语言通用编程规范》中有一段描述如下:
优先编译时检查错误
通过编译器来优先保证代码健壮性,而不是通过编写错误处理代码来处理编译就可以发现的异常,比如:
- 通过const来保证数据的不变性,防止数据被无意修改;
- 通过gsl::span等来保证char数组不越界,而不是通过运行时的length检查;
- 通过static_assert来进行编译时检查;
用通俗一点的语言表达就是“能使用机器进行检查的,就不要用人来保证其安全性”,如现在正如日中天的RUST语言所倡导的理念一样,要保证线程安全和内存安全,最好是在编译的时候就保证好,也就是说在设计的时候就要按照一定的规则进行设计,这样才能避免运行后发生错误。
下面对上面提到的三个部分进行详细的描述:
用const来保证数据的不变性
在《Effective C++》中的条款2和条款3中对const做了详细的描述,这里不再进行描述。
使用span来处理数组类型退化和越界访问问题
类型退化
C/C++中,在函数的参数传递中,如果将数组传入函数时,数组的类型会被退化成指针,如下所示:
void test(int a[]) {
cout << a[0] << endl;
}
int a[2] = {0};
在test函数中,打印a的第一个元素的值,因为a已经退化成了一个指针,如果没有数组大小的传递,我们并不知道a中的元素的个数有多少,一旦访问了超过其大小的元素,将出现越界的错误。
所以,一般我们的使用方法如下所示:
void test(int a[], int size) {
cout << a[size - 1] << endl;
}
int a[2] = {0};
使用span
基于上面的问题,在C++20中将span引入了标准。
在cppreference中,对span的描述如下:
The class template span describes an object that can refer to a contiguous sequence of objects with the first element of the sequence at position zero. A span can either have a static extent, in which case the number of elements in the sequence is known at compile-time and encoded in the type, or a dynamic extent.
span的实现基本如下所示:
template <typename T>
struct span
{
T * ptr_to_array; // pointer to a contiguous C-style array of data
// (which memory is NOT allocated or deallocated
// by the span)
std::size_t length; // number of elements in the array
// Plus a bunch of constructors and convenience accessor methods here
}
按照上面的代码和描述,span实际上就是一个指向连续序列对象的指针和长度的数据结构。有了span,我们上面test的代码就修改成下面的样子:
#include <span>
using namespace std;
void test(span<int> a) {
for (auto& x : a) {
cout << x << endl;
}
}
按照编程规范中的描述,如果访问超过a大小的元素,会报编译错误,但是我在ubunt 20.04上使用gcc 10.2.0进行测试,如果越界并不会报编译错误,可能标准中并没有对此进行修改,gsl的span实现可能会包含。
用static_assert来进行编译时检查
断言
在“百度百科”中搜索断言,会有下面的一些描述:
编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。
可见,我们使用断言一般实在调试或是单元测试的时候,断言可以保证我们的代码更加健壮,但是我们在调试和单元测试中使用的断言一般都是运行时断言。而有些时候我们需要在编译的时候对一些常量表达式进行判断,这样可以提前发现和识别问题。
static_assert
C++11引入了static_assert关键字,用于编译期间的断言,也叫静态断言。
用法:
static_assert(constant expression, comment);
第一个参数必须是一个常量表达式,因为是编译期间的断言,所以并不能去检查运行时的变量;第二个参数是第一个参数为false时候的提示字符串信息。
使用举例:
struct s {
int a; // long long a;
int b;
};
void test(const s& one) {
static_assert(sizeof(one.a) == 4, "the size of a is not 4");
}
如上例所示,在test函数中,我们对参数one中的变量a的大小进行编译期检查,上面的代码的编译结果没有问题,但是如果在未来,某位程序员对s中a的类型做了修改,修改成long long类型,那么编译就会出错,如下所示:
test.cpp: In function ‘void test(const s&)’:
test.cpp:23:30: error: static assertion failed: The size of one.a is not 4!
23 | static_assert(sizeof(one.a) == 4, "The size of one.a is not 4!");