2.5.2 静态断言与static_assert
通过2.5.1节的例子可以看到,断言assert宏只有在程序运行时才能起作用。而#error只在编译器预处理时才能起作用。有的时候,我们希望在编译时能做一些断言。比如下面这个例子,如代码清单2-7所示。
代码清单2-7
include <cassert>
using namespace std;
//枚举编译器对各种特性的支持,每个枚举值占一位
enum FeatureSupports{
C99=0x0001,
ExtInt=0x0002,
SAssert=0x0004,
NoExcept=0x0008,
SMAX=0x0010,
};
//一个编译器类型,包括名称、特性支持等
struct Compiler{
const char*name;
int spp;//使用FeatureSupports枚举
};
int main(){
//检查枚举值是否完备
assert((SMAX-1)==(C99|ExtInt|SAssert|NoExcept));
Compiler a={"abc",(C99|SAssert)};
//…
if(a.spp&C99){
//一些代码…
}
}
//编译选项:g++2-5-2.cpp
代码清单2-7所示的是C代码中常见的“按位存储属性”的例子。在该例中,我们编写了一个枚举类型FeatureSupports,用于列举编译器对各种特性的支持。而结构体Compiler则包含了一个int类型成员spp。由于各种特性都具有“支持”和“不支持”两种状态,所以为了节省存储空间,我们让每个FeatureSupports的枚举值占据一个特定的比特位置,并在使用时通过“或”运算压缩地存储在Compiler的spp成员中(即bitset的概念)。在使用时,则可以通过检查spp的某位来判断编译器对特性是否支持。
有的时候这样的枚举值会非常多,而且还会在代码维护中不断增加。那么代码编写者必须想出办法来对这些枚举进行校验,比如查验一下是否有重位等。在本例中程序员的做法是使用一个“最大枚举”SMAX,并通过比较SMAX-1与所有其他枚举的或运算值来验证是否有枚举值重位。可以想象,如果SAssert被误定义为0x0001,表达式(SMAX-1)==(C99|ExtInt|SAssert|NoExcept)将不再成立。
在本例中我们使用了断言assert。但assert是一个运行时的断言,这意味着不运行程序我们将无法得知是否有枚举重位。在一些情况下,这是不可接受的,因为可能单次运行代码并不会调用到assert相关的代码路径。因此这样的校验最好是在编译时期就能完成。
在一些C++的模板的编写中,我们可能也会遇到相同的情况,比如下面这个例子,如代码清单2-8所示。
代码清单2-8
include <cassert>
include <cstring>
using namespace std;
template<typename T,typename U>int bit_copy(T&a,U&b){
assert(sizeof(b)==sizeof(a));
memcpy(&a,&b,sizeof(b));
};
int main(){
int a=0x2468;
double b;
bit_copy(a,b);
}
//编译选项:g++2-5-3.cpp
代码清单2-8中的assert是要保证a和b两种类型的长度一致,这样bit_copy才能够保证复制操作不会遇到越界等问题。这里我们还是使用assert的这样的运行时断言,但如果bit_copy不被调用,我们将无法触发该断言。实际上,正确产生断言的时机应该在模板实例化时,即编译时期。
代码清单2-7和代码清单2-8这类问题的解决方案是进行编译时期的断言,即所谓的“静态断言”。事实上,利用语言规则实现静态断言的讨论非常多,比较典型的实现是开源库Boost内置的BOOST_STATIC_ASSERT断言机制(利用sizeof操作符)。我们可以利用“除0”会导致编译器报错这个特性来实现静态断言。
define assert_static(e)\
do{\
enum{assertstatic_=1/(e)};\
}while(0)
在理解这段代码时,读者可以忽略do while循环以及enum这些语法上的技巧。真正起作用的只是1/(e)这个表达式。把它应用到代码清单2-8中,就会得到代码清单2-9。
代码清单2-9
include <cstring>
using namespace std;
define assert_static(e)\
do{\
enum{assertstatic_=1/(e)};\
}while(0)
template<typename T,typename U>int bit_copy(T&a,U&b){
assert_static(sizeof(b)==sizeof(a));
memcpy(&a,&b,sizeof(b));
};
int main(){
int a=0x2468;
double b;
bit_copy(a,b);
}
//编译选项:g++ -std=c++11 2-5-4.cpp
结果如我们预期的,在模板实例化时我们会得到编译器的错误报告,读者可以实验一下在自己本机运行的结果。在我们的实验机上会输出比较长的错误信息,主要信息是除零错误。当然,读者也可以尝试一下Boost库内置的BOOST_STATIC_ASSERT,输出的主要信息是sizeof错误。但无论是哪种方式的静态断言,其缺陷都是很明显的:诊断信息不够充分,不熟悉该静态断言实现的程序员可能一时无法将错误对应到断言错误上,从而难以准确定位错误的根源。
在C++11标准中,引入了static_assert断言来解决这个问题。static_assert使用起来非常简单,它接收两个参数,一个是断言表达式,这个表达式通常需要返回一个bool值;一个则是警告信息,它通常也就是一段字符串。我们可以用static_assert替换一下代码清单2-9中bit_copy的声明。
template<typename t,typename u>int bit_copy(t&a,u&b){
static_assert(sizeof(b)==sizeof(a),"the parameters of bit_copy must have
same width.");
};
那么再次编译代码清单2-9的时候,我们就会得到如下信息:
error:static assertion failed:"the parameters of bit_copy should have same width."
这样的错误信息就非常清楚,也非常有利于程序员排错。而由于static_assert是编译时期的断言,其使用范围不像assert一样受到限制。在通常情况下,static_assert可以用于任何名字空间,如代码清单2-10所示。
代码清单2-10
static_assert(sizeof(int)==8,"This 64-bit machine should follow this!");
int main(){return 0;}
//编译选项:g++ -std=c++11 2-5-5.cpp
而在C++中,函数则不可能像代码清单2-10中的static_assert这样独立于任何调用之外运行。因此将static_assert写在函数体外通常是较好的选择,这让代码阅读者可以较容易发现static_assert为断言而非用户定义的函数。而反过来讲,必须注意的是,static_assert的断言表达式的结果必须是在编译时期可以计算的表达式,即必须是常量表达式。如果读者使用了变量,则会导致错误,如代码清单2-11所示。
代码清单2-11
int positive(const int n){
static_assert(n>0,"value must>0");
}
//编译选项:g++ -std=c++11-c 2-5-6.cpp
代码清单2-11使用了参数变量n(虽然是个const参数),因而static_assert无法通过编译。对于此例,如果程序员需要的只是运行时的检查,那么还是应该使用assert宏。