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关键字实现,传入待判定的编译期可解的表达式以及断言失败时上报的错误信息

相关内容

热门资讯

疑似新模型海外惊艳!智谱再度飙... 格隆汇2月10日|延续昨日强势,港股市场AI概念股今日再度集体走强,其中,“全球大模型第一股”智谱(...
原创 特... 特朗普上任已逾一年,他推行的关税政策像一阵狂风,搅动了全球的经贸秩序。对于美国经济的未来走向,诺贝尔...
原创 一... 2026年2月9日晚的美股市场,上演了一场让很多投资者既兴奋又意外的行情。 本以为大涨之后总要歇一歇...
电商领域侵权问题获关注,知识产... 2月10日,知识产权保护概念持续拉升,截至发稿,成分股读客文化(301025.SZ)、中文在线(30...
原创 1... 12艘满载着俄罗斯乌拉尔原油的超级油轮,正像一群迷路的巨鲸,散落在从马六甲海峡到中国南海的广阔水域里...
凯思凯迪完成近5亿融资:中平资... 雷递网 乐天 2月10日 凯思凯迪宣布近期完成近5亿元新一轮融资,本轮融资由中平资本领投,国寿资本、...
美国出现小米YU7测试车?雷军... 近日,网上传出小米YU7 MAX测试车出现在美国道路的消息,难不成小米汽车要进军美国市场了? 事实...
2026-2032年中国食糖行... 共研网发布的《2026-2032年中国食糖行业深度调研与市场调查预测报告》共十二章。首先介绍了食糖行...
原创 美... 特朗普上台后不久,便对进口产品挥起了关税大棒。从钢铝到汽车零部件,一系列严苛的关税政策自2025年春...
盘中必读|字节旗下Seedan... 2月10日,AI短剧概念延续强势,荣信文化(301231)、捷成股份(300182)、欢瑞世纪(00...
2月25日起预约!申请退税别错... 近日,国家税务总局发布通告,明确2025年度个人所得税综合所得汇算清缴办理时间为2026年3月1日至...
再迎反弹!现货黄金重回5000... 贵金属再迎反弹。 2月9日,黄金、白银价格同步拉升。现货黄金再次突破关键阻力位,重回5000美元/盎...
YU7现身加州高速,小米会不会... 2月10日,雷军发文: 前段时间,一辆YU7行驶在美国加州的高速公路上,挂着当地的测试车牌。 很多人...
宁波迎来开年第一股!爱芯元智港... 转自:东南财金 2月10日,爱芯元智(0600.HK)正式于港交所主板挂牌上市,成为港股边缘计算AI...
2026年春节档新片预售票房已... 2月10日,市场早盘窄幅震荡,三大指数小幅下跌,北证50指数盘中跌超1%。沪深两市半日成交额1.39...
原创 俄... 俄罗斯黄金大量涌入中国,这背后究竟隐藏了怎样的玄机?根据2025年海关的数据,单单实物净进口量就高达...
亚太药业:聘任邱中勋为公司总经... 每经AI快讯,亚太药业2月9日晚间发布公告称,因公司控制权已发生变更,根据《股份转让协议》约定等相关...
原创 中... 我们中国的女富豪中,不乏靠着刻苦努力一步步爬上顶端的典型,也有不少依靠精准眼光与幸运投资一跃而成的成...
黄金交易提醒:美元疲软+央行“... 汇通财经APP讯——2026年2月的第二个星期,全球金融市场的心脏,似乎正随着那剧烈跳动。金价在50...
多措并举推动投资止跌回稳 国家统计局数据显示,2025年,全国固定资产投资同比下降3.8%。分领域看,基础设施投资下降2.2%...