了解编程规范背后的故事(1)优先编译时检查错误

《华为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!");
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值