C++断言 static_assert, complie_assert, preComplie_assert

C++三类断言

文章目录

  • C++三类断言
    • 前置
      • 为什么要用断言?
      • 如何使用断言?
      • 注意
        • 避免使用断言去检查程序错误
        • 避免在断言表达式中使用改变上下文的语句
        • 异常处理
          • 获取错误代码errno
        • 避免使用goto语句
        • 避免使用setjmp与longjmp
        • 小结
  • 三类断言
    • 运行期间断言
    • 编译期间断言
    • 预编译期间断言
      • 样例
  • 总结

前置

为什么要用断言?

  • 首先要搞清楚为什么要用断言,不能看别人代码中有,就追赶时髦地用一用!从效果上来说assert断言能用if语句替换,那么为什么不用if语句把断言替换呢?一般而言,if语句是处理逻辑上的可能会发生的错误,断言则用来处理不应该发生的状况。
  • 什么是不应该发的的状况呢?这要区分数据的来源:1、数据来源于系统内部(子程序、子模块间的调用)2、数据来源于系统外部(外部设备如键盘的输入、串口数据的读取、网络数据的读取)。对内部来源的数据,我们没法去通过常规的测试手段去验证,此时断言就用上了。
  • 当然你如果硬是要用if语句也没人说你不对,但大量的if语句出现在源码中时,会造成代码臃肿,降低了可读性,另外会产生不紧凑代码,影响效率。
  • 程序开发初期,码农们忽视的是程序间调用参数的合法性,对这些参数可使用断言来防止意外,随着程序进入release版时,可以定义NDEBUG来让断言失效。以下是NDEGBU对assert的处理代码。
#ifdef NDEBUG

#define assert(expr)  (static_cast<void> (0))

#else

......

#endif

如何使用断言?

assert宏是在标准库中提供的。它在库文件中声明,它可以在程序中测试逻辑表达式,如果指定的逻辑表达式是false,assert()就会终止程序,并显示诊断消息。关闭断言使用#define NDEBUG,该语句会忽略转换单元中的所有断言语句。而且这个指令仅放在#include 之前才有效。示例如下:

#include 
#define NDEBUG     //关闭所有断言,必须放在#include 之前
#include 
using namespace std;
 
int main()
{
    int a = 10, b = 2;
    //使用断言,若assert()中为false,则程序终止退出
    assert(a < b);
    cout << a << b << endl;
    return 0;
}

注意

避免使用断言去检查程序错误

在断言的使用中,应该遵循这样的一个规定:对来自系统内部的可靠数据使用断言,对于外部不可靠数据不能使用断言,而应该使用错误处理代码。

换句话而言,断言是用来处理不应该发生的非法情况,而对于可能发生的应该使用错误处理代码。

对于用户输入,与外部系统进行协议交互时的情况,也不能使用断言进行参数的判断,这种情况属于正常的错误检查。

下面的例子说明了断言的使用场景

char * Strdup(const char * src){
    assert(src!=NULL);

    char * result = NULL;
    size_t len = strlen(src) +1;
    result = (char *)malloc(len);

    assert(result != NULL);
    return result;
}

复制

例子中第一个断言assert(src!=NULL)用于判断传入的参数的正确性,保证参数不为NULL 第二个断言assert(result != NULL)检查函数返回值是否为NULL。

例子中的两个断言,第一个是合法的,而第二个不合法,第一个合法是因为传入的参数必须不为NULL,断言如果成功,则说明调用代码存在问题,这属于非法的情况,此处属于断言的正确使用情况。

第二个断言则不同,malloc对于返回NULL的情况属于调用正常情况,这应该使用正常的错误处理逻辑,不应该使用断言。

避免在断言表达式中使用改变上下文的语句

在assert宏只有在Debug版本中情况下,应该避免断言表达式中使用改变环境的语句。

如下例子因为断言语句的缘故,将导致不同的编译版本产生不同的结果。

int test(int i)
{
    assert(i++);
    return i;
}

复制

因此应该避免在断言表达式中使用改变上下文环境的语句,也就是确保断言仅仅作为一个检查而存在,不应该参与正常语句的处理。

异常处理
获取错误代码errno

error 是用于表达不同错误值的一个全局变量。如果一个系统调用或库函数调用失败,可以通过errno的值来确定问题所在。

因errno是一个全局变量,在调用不同系统调用或者库函数失败时都有可能修改它的值,因为在使用errno时,应先将其清0

    errno = 0;

    FILE *fp = fopen("test.txt", "r");
    if (fp == NULL) {

        if (errno!=0) {
            printf("error : %d \n",errno);
            printf("错误信息 : %s \n",strerror(errno));
        }

    }

复制

但errno并不是所有的库函数都适合使用,就error而言库函数一般分为如下几种。

1.函数返回值无法判断错误,需进一步从errno中获取错误信息

函数 返回值 errno值
fgetwc、fputwc WEOF EILSEQ
strtol、wcstol LONG_MIN或LONG_MAX ERANGE
strtoll、wcstoll LLONG_MIN或LLONG_MAX ERANGE
strtoul、wcstoul ULONG_MAX ERANGE
strtoull、wcstoull ULLONG_MAX ERANGE
strtoumax、wcstoumax UINTLLONG_MAX ERANGE
strtod、wcstod 0或者±HUGE_VAL ERANGE
strtof、wcstof 0或者±HUGE_VALF ERANGE
strtold、wcstold 0或者±HUGE_VALL ERANGE
strtoimax、wcstoimax IMAX_MIN或INTMAX_MAX ERANGE

以字符串转成长整型函数strtol为例, 在64位机器下,long长度为8字节,最大值LONG_MAX 为 0x7fffffffffffffff,当变量longStr 取超出长整型最大值的字符串”0xffffffffffffffff”和刚好等于最大值的字符串”0x7fffffffffffffff”时,函数的返回值都为相同的LONG_MAX。此时金聪返回值是无法判断函数的执行的成功与否。这个时要判断errno的值。如下例中,会打印出错误的信息。

    errno =0;

    //LONG_MAX的最大值为0x7fffffffffffffff
    const char * longStr = "0xffffffffffffffff";
    long ret = strtol(longStr,NULL,16);
    if (ret == LONG_MAX) {
        if (errno!=0) {
            printf("error : %d \n",errno);
            printf("错误信息 : %s \n",strerror(errno));
        }else{
            printf("等于long的最大值\n");
        }
    }

复制

2.函数返回值可知错误,errno可知更详细的错误

函数 返回值 errno值
ftell() -1L positive
fgetpos()、fsetpos() nonzero positive
mbrtowc()、mbsrtowcs() (size_t)(-1) EILSEQ
signal() SIG_ERR positive
wcrtomb()、wcsrtombs (size_t)(-1) EILSEQ
mbrtoc16()、mbrtoc32() (size_t)(-1) EILSEQ
c16rtomb()、cr21rtomb (size_t)(-1) EILSEQ

3.有不同标准文档的库函数

有些函数在不同的标准下对errno有不同的定义,例如fopen中便是一个例子。C99并没有对使用fopen是对errno做要求,但POSIX.1却声明了错误时返回NULL,并将错误码写入errno。

避免使用goto语句

goto语句有很多优点,例如goto语句可以非常方便的在局部作用域中跳出多层循环,执行如无条件的跳转。 但正因为goto语句可以灵活的跳转,如果不加以限制它会破坏程序的结构化风格,使得代码难以理解与测试,同时不加限制的使用goto语句可能跳过变量的初始化、重要的计算等语句。

以下例子在a小于0或者a小于等于100时会使用goto跳转到标记为Error的语句中。

注意goto只能在局部作用域中跳转。

void testGoto(int a)
{
    if (a>0) {
        if (a>100) {
            printf(" a = %d \n",a);
        }else{
            goto Error;
        }
    }else{
        goto Error;
    }
Error:
    printf("Test Error a = %d \n",a);
}

复制

避免使用setjmp与longjmp

相比与goto语句只能在局部作用域中跳转,setjump与longjmp可以进行跨作用域跳转,也就是跨函数跳转。

我们知道函数调用都以函数栈的形式进行调用与退出,既然要做到跨函数跳转,那便需要对当前的函数栈进行保存与还原,而setjmp的作用便是保存当前函数栈至类型jmp_buf结构体变量中,而longjmp的作用便是从此结构体中恢复,还原函数栈。

而相对于goto仅在作用域内跳转,setjmp和longjmp则使代码更加的难以维护以及可读。

小结
  1. C语言中,使用函数的返回值来标志函数是否执行成功(默认成功返回1,失败返回0)当使用接口时,必须对函数进行正确性的验证,检查它的返回值,并且对每个错误的返回值进行相应的处理以及提示。
  2. 同样的道理,如果作为接口的开发方,需要对函数的各种情况反映到返回值中。
  3. 编写代码是,无论使用什么样的错误处理方式,发现程序中错误最好的方法便是执行程序,让数据在函数中流动,在判断逻辑中查找到函数出错的地方。

三类断言

运行期间断言

在C++中,标准在 或vassert.h>头文件中为程序员提供了 assert宏,用于在运行时进行断言。前面错误示例改成assert宏实现就正确了。与静态断言使用的 static_assert 不同,assert 并不支持自定义错误消息。

// #define NDEBUG
#include 
#include 
template <typename T>
T *array_alloc(unsigned int size)
{
    assert(size > 0);
    return new T[size];
}
int main()
{
    std::cout << "main start" << std::endl;
    int *ah = array_alloc<int>(0);
    // 编译通过但,运行时候报错
    std::cout << "main end" << std::endl;

    // 输出结果
    //     main start
    // Assertion failed: size > 0, file e:\CodeFile\CPP_SINGLE\ASSERT\runningTime_assert\1-runtime_assert.cpp, line 6
}
//  需要注意的是,C 程序中的运行时断言是否可用,也会受到宏常量 NDEBUG 的影响。
//  当该宏常量的定义先于 #include  语句出现时,编译器会忽略对 assert 宏函数调用代码的编译。
//  反之,它便会在程序运行时进行正常的断言检查。通过这种方式,我们可以相对灵活地控制运行时断言的启用与关闭。
//  事实上,assert宏在中的实现方式类似:

  • 需要注意的是,C 程序中的运行时断言是否可用,也会受到宏常量 NDEBUG 的影响。当该宏常量的定义先于 #include 语句出现时,编译器会忽略对 assert 宏函数调用代码的编译。反之,它便会在程序运行时进行正常的断言检查。通过这种方式,我们可以相对灵活地控制运行时断言的启用与关闭。事实上,assert宏在中的实现方式类似:
#ifdef NDEBUG
#define assert(expr)    (static_cast<void>(0))
#else
//其他
#endif
        类似

编译期间断言

  • 在C++11标准中,引入了 static_assert断言来解决这个问题。static_assert使用起来非常简单,它接收两个参数,一个是断言表达式,这个表达式通常需要返回一个bool值;一个则是警告信息,它通常也就是一段字符串。
#include 
#include 
#include 
#include 
using namespace std;
template <typename T, typename U>
void bit_copy(T &a, U &b)
{
    static_assert(sizeof(a) == sizeof(b), "a and b must be equal");
    memcpy(&a, &b, sizeof(a));
}
int main()
{
    int a = 10;
    int b = 30;
    bit_copy(a, b);
    cout << "ok" << endl;
}
  • 处理的表达式必须在编译期间就能知道结果。

    另外必须注意的是,static_assert的断言表达式的结果必须是在编译时期可以计算的表达式,即必须是常量表达式。如果读者使用了变量,则会导致错误,如:

int bit_deal (int a) ( 
	static_assert( a>=1,"the parameters of a should >=1.");
);

​ 上面使用了参数变量a ,因而static_assert无法通过编 译。如果需要对变量进行检查,就要是实现运行时的检查,使用assert宏了。

  • 该关键字的用法类似C++11标准之前开源库 Boost内置的BOOST_STATIC_ASSERT断言机制类似,利用1/(e)这个表达式来判定:

    
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    #define cp_assert(e)                \
        do                              \
        {                               \
            enum                        \
            {                           \
                assert_static = 1 / (e) \
            };                          \
            \ 
                                                                                                                                                                                                                                                                                                                    \
        } while (0)
    
    template <typename T, typename U>
    void bit_copy(T &a, U &b)
    {
        cp_assert(sizeof(a) == sizeof(b));
        memcpy(&a, &b, sizeof(a));
    }
    int main()
    {
        int a = 10;
        int b = 30;
        bit_copy(a, b);
        cout << "ok" << endl;
    }
    
  • ​ 在通常情况下,static_assert可以用于任何名字空间,在预处理阶段,static_assert 宏会被展开成名为 _Static_assert 的 C 关键字。该关键字以类似“函数调用”的形式在 C 代码中使用,它的第一个参数接收一个常量表达式。程序在被编译时,编译器会对该表达式进行求值,并将所得结果与数字 0 进行比较。若两者相等,则程序终止编译,并会将通过第二个参数指定的错误信息,与断言失败信息合并输出。若两者不相等,程序会被正常编译,且该关键字对应的 C 代码不会生成任何对应的机器指令。

预编译期间断言

在 c/c++代码中,我们常能看到名为 errno 的预处理器宏:

    1)#error 是一种预编译器指示字,用于生成一个编译错误消息 。
    2)#error [message] //message为用户自定义的错误提示信息,可缺省。
    3)#error 编译指示字用于自定义程序员特有的编译错误消息。
    4)#error 可用于提示编译条件是否满足。编译过程中的任何错误意味着无法生成最终的可执行程序。

    它常用的用法就是,通过预处理指令#if和 #error的配合,可以让程序员在预处理阶段进行断言。例如我们可以在程序中判断某个自定义宏是否存储而进行断言提示:

#ifndef MY_OPT
#error “MY_OPT not define in code, include instead.”
#endif
如果程序中没有包含需要的头文件并进行编译,该头文件内的宏_MY_OPT_就无法匹配到而引发错误。#error指令会将后面的语句输出,从而提醒用户要使用这个头文件,这样一来,通过预处理时的断言,发布者就可以避免一些头文件的引用问题。

    类似的,还有#warning 这种用于生成编译警告消息预处理宏。

样例

main.cpp

#include "primary.h"

int main()
{
    say_hi();
    return 0;
}

primary.h

#ifndef _PRIMARY_H
#define _PRIMARY_H
#endif
#include "secondary.h" // 模拟 primary  依赖secondary 头文件的内容
// ......... 包含其他处理的代码
//. .........

secondary.h

#ifndef _PRIMARY_H

#error Never use secondary.h directly.Include primary.h instead.
#endif
#include 
void say_hi()
{
    printf("say hi\n");
}

如果再main 中只包含 secondary.h 而没有 包含 primary.h

#include "secondary.h"

int main()
{
    say_hi();
    return 0;
}
In file included from e:\CodeFile\CPP_SINGLE\ASSERT\3PreCompilie_assert\mian.cpp:1:
e:\CodeFile\CPP_SINGLE\ASSERT\3PreCompilie_assert\secondary.h:3:2: error: #error Never use secondary.h directly.Include primary.h instead.
    3 | #error Never use secondary.h directly.Include primary.h instead.
      |  ^~~~~

总结

  • \1. 断言(assertion)分类:运行时、预编译期、编译期,即断言的判断触发时机
  • \2. 运行时断言使用assert宏来实现
  • \3. 一旦定义了NDEBUG宏,那么assert宏将被展开成一条空语句不起作用,(可能会)被编译器优化掉,原意是在非Debug编译下不把运行时assert编进来
  • \4. 预编译时期的断言可以使用预处理命令#ifdef或#ifundef配合#error实现,可以在预编译时期对一些宏进行检查以确定编译参数、头文件引用状态等
  • \5. 编译期断言在C++11之前可以使用除0操作来实现,传入编译期可判定的表达式,则在编译期可计算出是true还是false(即0),然后除0可在编译期报告错误
  • \6. 使用上一条中的方式拿到的是除0错误,语义不明朗,在C++11中编译期断言可直接用static_assert关键字实现,传入待判定的编译期可解的表达式以及断言失败时上报的错误信息
```







# 总结

- \1. 断言(assertion)分类:运行时、预编译期、编译期,即断言的判断触发时机
- \2. 运行时断言使用assert宏来实现
- \3. 一旦定义了NDEBUG宏,那么assert宏将被展开成一条空语句不起作用,(可能会)被编译器优化掉,原意是在非Debug编译下不把运行时assert编进来
- \4. 预编译时期的断言可以使用预处理命令#ifdef或#ifundef配合#error实现,可以在预编译时期对一些宏进行检查以确定编译参数、头文件引用状态等
- \5. 编译期断言在C++11之前可以使用除0操作来实现,传入编译期可判定的表达式,则在编译期可计算出是true还是false(即0),然后除0可在编译期报告错误
- \6. 使用上一条中的方式拿到的是除0错误,语义不明朗,在C++11中编译期断言可直接用static_assert关键字实现,传入待判定的编译期可解的表达式以及断言失败时上报的错误信息

你可能感兴趣的:(C++,c++,开发语言)