C++之constexpr和常量表达式
1. 常量表达式简介
常量表达式(const expression) 是指不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的 const
对象也是常量表达式。后面将会提到,C++语言中有几种情况下是要用到常量表达式的。
一个对象(或表达式)是不是常量表达式是由它的数据类型和初始值共同决定,例如:
const int max_files = 20; // max_files 是常量表达式
const int limit = max_files + 1; // limit 是常量表达式
int staff_size = 27; // staff_size 不是常量表达式
const int sz = get_size(); // sz 不是常量表达式
尽管 staff_size
的初始值是一个字面值常量,但由于它的数据类型只是一个普通 int
而非 const int
,所以它不属于常量表达式。另一方面,尽管 sz
本身是一个常量,但它的具体值知道运行时才能获取到,所以也不是常量表达式。
2. constexpr 变量
在一个复杂系统中,很难分辨一个初始值到底是不是常量表达式。当然可以定义一个 const
变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。可以这么说,在此种情况下,对象的定义和使用是两回事。
C++11 新标准中规定,允许将变量声明为 constexpr
类型以便有编译器来验证变量的值是否是一个常量表达式。声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20; // 20 是常量表达式
constexpr int limit = mf + 1; // mf + 1 是常量表达式
constexpr int sz = sizes(); // 只有当 size 是一个 constexpr 函数时才是一条正确的声明语句
尽管不能使用普通函数作为 constexpr
变量的初始值,但是新标准中允许定义一种特殊的 constexpr
函数(在后面会介绍)。这种函数应该是足够简单以使得编译时就可以计算其结果,这样就能用 constexpr
函数去初始化 constexpr
变量了。
note: 一般来说,如果你认定一个变量是一个常量表达式,那就把它声明为 constexpr
类型
3. 字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明constexpr
时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型” (literal type)。
到目前位置接触过的数据类型中,算数类型、引用和指针都属于字面值类型(其中数据成员都是字面值类型的聚合类是字母值常量类)。自定义类、IO库、string类型则不属于字面值类型,也就不能定义成constexpr
。
尽管指针和引用都能定义成 constexpr
,但他们的初始值却收到严格限制。一个constexpr
指针的初始值必须是nullptr
或者 0 ,或者是存储于某个固定地址中的对象。其中需要注意的是,函数体内定义的变量一般来说并非存放在固定的地址中,因此constexpr
指针不能指向这样的变量。相反,定义于所有函数体之外的对象其地址固定不变,能用来初始化 constexpr
指针。
note1:其中数据成员都是字面值类型的聚合类是字母值常量类。如果一个类不是聚合类,但它符合下述要求,则它也是一个字面值常量类:
- 数据成员必须都是字面值类型。
- 类必须至少含有一个constexpr构造函数
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类型,则初始值必须使用成员自己的constexpr构造函数。
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
note2:
尽管构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数。事实上,一个字面值常量类必须至少提供一个 constexpr 的构造函数。此时constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式。其中constexpr构造函数用于生产constexpr对象以及constexpr函数的参数或返回类型:
4. 指针和constexpr
必须明确一点,在constexpr
声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关:
const int *p = nullptr; // p 是一个指向 整形 常量的指针
constexpr int *q = nullptr; // q 是一个指向 整数 的常量指针
int k = 99;
void test02() {
int i = 10, j = 20;
const int *p = &i; // right,p 是一个指向 整形 常量的指针
constexpr int *q = nullptr; // q 是一个指向 整数 的常量指针
constexpr int *q = &j; // error: '& j' is not a constant expression
constexpr int *q = p; // error: 'p' was not declared 'constexpr'
constexpr int *q2 = q; // right
constexpr int *q3 = &k; // right, 因为此时k定义在函数体外,为一个常量值
p = &i; // right
q = &j; // error: assignment of read-only variable 'q'
cout << *p << endl; // 10
// cout << *q << endl;
}
其中p和q的类型相差甚远,p是一个指向常量的指针,而q是一个常量指针,其中的关键在于 constexpr
把它定义的对象置位了顶层 const。
与其他常量指针类型,constexpr
指针既可以指向常量也可以指向一个非常亮。
constexpr int *np = nullptr; // np 是一个指向整数的常量指针,其值为空
int j =0;
constexpr int i = 42; // i 的类型是整形常量
// i和j都必须定义在函数体之外
constexpr const int *p = &i; // p 是常量指针,指向整形常量 i
constexpr int *p1 = &j; // p1 是常量指针,指向整数 j
5. constexpr函数
constexpr 函数(constexpr function)是指能用于常量表达式的函数。定义 constexpr 函数的方法与其他函数类型,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); // right,foo是一个常量表达式
此时 new_sz()
为一个无参的constexpr 函数。因为编译器能在程序编译时验证new_sz
函数返回的是常量表达式,所以可以用 new_sz
函数初始化 constexpr 类型的变量foo。
执行该初始化任务时,编译器把对 constexpr 函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr 函数被隐式地指定为内联函数。
constexpr 函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr 函数中可以有空语句、类型别名(typedef)以及 using 声明。
C++中允许 constexpr 函数的返回值并非一个常量:
// 如果 arg 是常量表达式,则scale(arg) 也是一个常量表达式
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
int arr[scale(2)]; // right, scale(2)是常量表达式
int i = 1; // i不是常量表达式
int a2[scale(i)]; // scale(i)不是常量表达式
cout << sizeof(arr) << endl; // 336 = 2 * 42 * 4
cout << sizeof(a2) << endl; // 168 = 1 * 42 * 4
cout << typeid(arr).name() << endl; // A84_i
// cout << typeid(a2).name() << endl; // error, cannot create type information for type ‘int [(<anonymous> + 1)]’ because it involves types of variable size
cout << typeid(scale(1)).name() << endl; // y
cout << typeid(i).name() << endl; // i
当scale 的实参是常量表达式时,它的返回值是常量表达式;反之则不然。
如上例所示,当给scale函数传入的一个字面值2的常量表达式时,它的返回类型也是常量表达式。此时,编译器用相应的结果值替换对scale函数的调用。
如果用一个非常量表达式调用scale函数,比如 int 类型的对象 i ,则返回值是一个非常量表达式。当把 scale 函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要。如果结果恰好不是常量表达式,编译器将会发出错误信息。
参考文献:《c++ primer 第五版》