C++断言 static_assert, complie_assert, preComplie_assert
admin
2024-05-20 11:20:08
0

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 (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、fputwcWEOFEILSEQ
strtol、wcstolLONG_MIN或LONG_MAXERANGE
strtoll、wcstollLLONG_MIN或LLONG_MAXERANGE
strtoul、wcstoulULONG_MAXERANGE
strtoull、wcstoullULLONG_MAXERANGE
strtoumax、wcstoumaxUINTLLONG_MAXERANGE
strtod、wcstod0或者±HUGE_VALERANGE
strtof、wcstof0或者±HUGE_VALFERANGE
strtold、wcstold0或者±HUGE_VALLERANGE
strtoimax、wcstoimaxIMAX_MIN或INTMAX_MAXERANGE

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

    errno =0;//LONG_MAX的最大值为0x7fffffffffffffffconst 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()-1Lpositive
fgetpos()、fsetpos()nonzeropositive
mbrtowc()、mbsrtowcs()(size_t)(-1)EILSEQ
signal()SIG_ERRpositive
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 
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(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(0))
#else
//其他
#endif类似

编译期间断言

  • 在C++11标准中,引入了 static_assert断言来解决这个问题。static_assert使用起来非常简单,它接收两个参数,一个是断言表达式,这个表达式通常需要返回一个bool值;一个则是警告信息,它通常也就是一段字符串。
#include 
#include 
#include 
#include 
using namespace std;
template 
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 
    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关键字实现,传入待判定的编译期可解的表达式以及断言失败时上报的错误信息

相关内容

热门资讯

斗金订购APP贵金属期货投资被...   斗金订购APP的投资者被广告宣传给诱导,注册就送什么现金,然后充值返现金卷等等这些宣传方式,都是...
哈易购APP非法期货交易欺骗投...   哈易购APP宣传可做白银铂金贵金属订购交易,但实际上并没有取得相关交易资质!哈易购APP本质上就...
消息称百度旗下昆仑芯瞄准500... 6 月 29 日消息,据《The Information》昨日援引知情人士消息,百度旗下 AI 芯片...
打造夏日消费新场景 第35届北... 北京商报讯(记者 翟枫瑞)6月29日消息,第35届北京国际燕京啤酒文化节新闻发布会在京举行。本届啤酒...
社保基金持仓数据出炉,一季度增... 最近各大上市公司一季度财报都公开了,咱们国家社保基金的持仓数据也全部曝光。目前社保拿着比亚迪价值44...
36氪首发 | 海思、中兴团队... 作者 | 乔钰杰 编辑 | 袁斯来 硬氪获悉,广州宸思通讯科技有限公司(以下简称“宸思科技”)近日完...
两天蒸发47亿市值!一纸税务通... 一纸税务通知书,能让一家百亿龙头两天蒸发47亿市值。 6月22日,北大荒(600598.SH)公告称...
SK海力士将投资1100万亿韩... SK集团会长崔泰源6月29日在韩国“三大重大计划”发布会上宣布,公司将投资1100万亿韩元扩大半导体...
两只A股,终止上市! 两家A股公司,即将摘牌。 6月29日,退市沪科(600608.SH)公告称,上海证券交易所将在202...
原创 M... 一家成立近十年的自动驾驶公司,在IPO时吸引了14家基石投资者认购近一半的发行股份,其中不乏奔驰、比...
基金忠言|国寿安保滤镜碎,三年... 图片来源:视觉中国 蓝鲸新闻6月29日讯(记者 祁和忠)保险系基金公司国寿安保总经理换人了。 6月2...
三星电机计划加码玻璃基板!相关... 6月29日,玻璃基板概念股午后有所回升, 华工科技(000988.SZ)逼近涨停, 彩虹股份(600...
拉萨海关持续壮大外贸经营主体 ...   新华网拉萨6月28日电(记者蒋梦辰)近日,记者从拉萨海关获悉,今年前5个月,西藏有进出口实绩的外...
机构:二季报临近,医药生物板块... 6月29日,华源证券发布了一篇医药生物行业的研究报告,报告指出,业绩期临近,产业链景气度有望再次迎来...
每日收评科创50放量涨超4.5... 财联社6月29日讯,三大指数全线收红,创业板指探底回升,科创50指数大涨4.61%。沪深两市成交额3...
6月多地土拍结构性升温:深圳单... 进入2026年6月,不少城市核心区地块集中诞生高溢价宗地,热度突出的城市包含深圳、杭州、长沙。 其中...
业绩炸裂!盛达资源半年预盈3.... 6月29日,贵金属矿山龙头盛达资源(000603.SZ)发布 2026 年半年度业绩预告,上半年业绩...
A股午后拉升三大股指收涨:半导... A股三大股指6月29日开盘涨跌互现。早盘沪强深弱,创指一度跌超2%。半导体午后拉升,带动两市上涨,沪...
原创 空... 前言 大家好,我是老金。 这几天,两幅极度割裂的画面放在一起,把我看笑了。 一边是在持续的热浪下,欧...