6.1 常量表达式
类别:类作者
6.1.1 运行时常量性与编译时常量性
在C++中,我们常常会遇到常量的概念。常量表示该值不可修改,通常是通过const关键字来修饰的。比如:
const int i=3;
上述代码就声明了一个名字为i的常量。const还可以修饰函数参数、函数返回值、函数本身、类等。在不同的使用条件下,const有不同的意义,不过大多数情况下,const描述的都是一些“运行时常量性”的概念,即具有运行时数据的不可更改性。不过有的时候,我们需要的却是编译时期的常量性,这是const关键字无法保证的。我们可以看看代码清单6-1所示的例子。
代码清单6-1
const int GetConst(){return 1;}
void Constless(int cond){
int arr[GetConst()]={0};//无法通过编译
enum{e1=GetConst(),e2};//无法通过编译
switch(cond){
case GetConst()://无法通过编译
break;
default:
break;
}
}
//编译选项:g++ -c 6-1-1.cpp
在代码清单6-1中,我们定义了一个返回常数1的函数GetConst。我们使用了const关键字修饰了返回类型。不过编译后我们发现,无论将GetConst的结果用于需要初始化数组Arr的声明中,还是用于匿名枚举中,或用于switch-case的case表达式中,编译器都会报告错误。发生这样错误的原因如我们上面提到的一样,这些语句都需要的是编译时期的常量值。而const修饰的函数返回值,只保证了在运行时期内其值是不可以被更改的。这是两个完全不同的概念。
我们再来看一个BitSet的例子,该例子更贴近开发人员的实际状况,如代码清单6-2所示。
代码清单6-2
enum BitSet{
V0=1<<0,
V1=1<<1,
V2=1<<2,
VMAX=1<<3
};
//重定义操作符"|",以保证返回的BitSet值不超过枚举的最大值
const BitSet operator|(BitSet x,BitSet y){
return static_cast<BitSet>(((int)x|y)&(VMAX-1));
}
template<int i=V0|V1>//无法通过编译
void LikeConst(){}
//编译选项:clang++-c-std=c++11 6-1-2.cpp
在代码清单6-2中,我们看到一个有限成员的BitSet的枚举类型的定义(读者是否还记得代码清单2-7所示的例子)。而为了尽可能地保证或操作的有效性,我们重载了operator,该操作除了进行或运算外,还会通过&(VMAX-1)这样的操作保证该或操作的输出不会超过VMAX枚举值。而此时我们将V0|V1作为非类型模板函数的默认模板参数,则会导致编译错误。这同样是由需要的是编译时常量所导致的。
那么有没有什么办法可以通过更改上面的例子,获得编译时期的常量呢?最简单的方法是,我们可以在代码清单6-1的文件开始使用C中的宏替代GetConst函数。
define GetConst 1
当然,这种简单粗暴的做法即使有效,也会把C++拉回“石器时代”。C++11中对编译时期常量的回答是constexpr,即常量表达式(constant expression)。比如我们要使得代码清单6-1中的GetConst函数成为一个常量表达式,可以用下面的声明方法:
constexpr int GetConst(){return 1;}
即在函数表达式前加上constexpr关键字即可。有了常量表达式这样的声明,编译器就可以在编译时期对GetConst表达式进行值计算(evaluation),从而将其视为一个编译时期的常量(虽然编译器不一定这么做,但至少从语法效果上看是这样,我们会在后面叙述)。这样一来代码清单6-1中的数组Arr、匿名枚举的初始化以及switch-case的case表达式通过编译都不再是问题(读者可以自行实验一下)。
在C++11中,常量表达式实际上可以作用的实体不仅限于函数,还可以作用于数据声明,以及类的构造函数等。我们来分别看一下。