1. 从static_assert 说起
事情得从C++11 新引入的 static_assert 说起。
static_assert 意为 “静态断言”。语法如下:
static_assert(编译期常量表达式,字面字符串常量);
其中 “编译期常量表达式” 得是一个在编译期间,就能求值的表达式,“字面字符串”指直接写在代码的字符串。前者是接受检查的条件,后者是条件不成立时,程序员想要借助编译器的输出内容的抱怨。
在C++的语境下,“静态 / static” 通常指程序编译期间的事情。static_assert 可以让程序在编译时期检查一些固定条件。程序都还没有运行起来,有什么固定条件可以检查呢?最常见的有两种:
- 编译的机器环境,比如是 64 位机,还是 32 位机;
- 代码中的数据类型。
我们以第一种情况举下例子:C++ 对 long (长整型)的宽度要求是:至少 32位(也就是4字节)。假设,我们写一个程序,只想在 long 长度为64位(8字节)的环境下编译通过,那么,使用static_assert 在编译期间检查的代码,示例如下:
#include <iostream>
int main()
{
static_assert(sizeof(long) >= 8, "long 在当前平台下,不够 long,不足 64 位");
}
这段代码在linux 等环境下,编译、运行都不会出问题。大家可以试试:远程linux运行,静态检查 long 长度 ≥ 8字节 。但在 Windows 下编译会失败,更谈不上运行:
注意红色的输出信息,有一部分正是我们提供给 static_assert() 的第二个入参……这看起来多么人性化啊!我们(程序员)把对环境的不满意,直接地、痛快地、淋漓尽致地表达出来的……
问题来了,存在数十年,来自 C 语言,本质是一个宏定义的 assert,只能有一个入参,也就是待检查的条件表达式,没有第二个入参,于是使用它的程序员就只能把抱怨、吐槽、不满的内容尽吞心情,长久下来,内分泌失调怎么办?
2. assert:何时用断言?
assert 就是“断言”,相比前面提到的新人(转眼都12年了) “static_assert”,它就是“动态”断言了,意思是,它是在程序运行期间内才开始“断案”。
多数C++程序员使用 static_assert 的机会并不多,但使用 assert 机会就很多,基本是必要的+编程的必备技能。
知识点来了!
程序运行难免会有错误,从原因上分析,通常有两大类:1、设计错误,2、实现错误。
比如,小丁想写一个用于判断某一年是不是闰年的工具软件,结果他不知道有 “每400年” 的规则,那么他实现出来的程序,当然就存在问题;这就叫设计错误。另一情况,小丁清楚知道如何判断闰年,可是在写代码时,一不小心,把 400 写成 40,这就叫实现错误。
现实中的程序BUG,主要是实现错误。而 assert 工具,也完全是为了减少实现错误。我们再举个例子——
假设小丁在写一个给大象体检的程序,需要知道大象的体重。现在不是三国,所以已经有可以直接称大象体重的地秤,并且这个地秤有接口和电脑对接,现在需要写一个函数,来对接接口,读取重量。小丁很忙,于是让小印去实现这个函数,两个约定好基本设计:这个函数返回一个整数,单位是公斤。
假设这个函数原型是 “int 读取大象体重() ” 小丁知道小印这人不是很靠谱,将单位搞错这种事他就经常犯,此时,在调用这个函数时,小丁就可以这样处理:
...
int kg = 读取大象体重();
asssert(kg > 15 && kg < 150000); // 断言1头大象体重应该在15公斤到150吨之间
// 开始使用大象体重 kg
...
没错,团队编程时,我们既要相信团队,又要不相信团队中的任何一个人,特别是你的上家。如果一个人写程序,就更不要过于相信自己。因此,对于团队内部已经约定好的条件,可以使用 assert 来检查,因为 assert 最大的特点就是:条件不符,我就敢让这个程序直接死给大家看。显然 assert 主要用在一个程序的内部调试阶段。所以这里的大家,是指团队内部。
如果条件来自最终用户的输入,就不能再使用 assert 这样粗暴的方式,而是要让程序做到以下三点(按重要性及优先级由高到低排列):
- 火眼金睛:能查出用户各种有意无意甚至恶意的乱输入,即不上当;
- 固若金汤:不因读到用户的错误输入而直接挂掉;
- 不卑不亢:同时还能给出合适的反馈,不臭脸(因为要考虑有些用户只是无意操作错误),也不当“舔狗”(因为要考虑有些“用户”就是恶意攻击的,程序使用“舔狗”式的反应,容易耗费过多资源,造成连正常用户也无法响应)
3. assert 小“缺陷”与解决
言归正传,相比 static_assert,assert 没有提供断言失败时消息说明,仅程序员无法吐槽其实是小事,主要是不方便从程序运行的异常结果(因为 assert 而异外退出),一眼看到问题的具体情况。
解决方法就是一个行业里惯用的小伎俩,以前面大象体重断言失败为例:
asssert((kg > 15 && kg < 150000)
&& "1头大象体重应该在15公斤到150吨之间!" );
...
现在,当 kg 的数值显然不对头时,程序会直接退出,并且屏幕上出现的断言失败的信息中,将含有“1头大象体重应该在15公斤到150吨之间!” 的描述。
如果你喜欢的话,可以将这句话前面的 && 改为 and (C++支持用 and 表示 &&,即:并且)。
asssert((kg > 15 && kg < 150000)
and "1头大象体重应该在15公斤到150吨之间!" );
简单地说,就是把原来的 assert(条件),改为 assert( 条件 and “断言失败时的描述” )
解释一下它的工作原理: C++的字符串数据,本质是一个指针,并且直接写在代码中的字符串,即“字面字符串常量”,肯定不会是一个空指针(因为肯定有数据),当布尔值时,就是永远为真。所以原有条件再加上 “并且永远为真”,完全不影响原有条件是否成立的判定。
比如:
assert (2 > 1 and "这怎么可能???"); // 真 并且 真 -> 真
永远不会看到 “这怎么可能???”,因为 2>1 一定成立,而:
assert (2 < 1 and "这怎么可能???"); // 假 并且 真 -> 假
则断言失败。
再来:
assert ("我是天下最帅的人" and "我是天下最富的人");
断言成功。不在于字符串的内容,在于字符串就是真。