0. 常量表达式的定义
常量表达式(const expression) 是指值不会改变,并且在编译过程中就能得到计算结果的表达式。
很显然,这是表达式的另一个属性,即表达式的结果是确定不变的。这也是一个极大的优化,因为在编译过程中就可以确定对象的值。我们可以给一些字面值常量或者使用字面值常量的操作赋予一些名称,便于搜索、修改。
我们之前使用的字面值常量(注意区分下面介绍的字面值类型)、由常量表达式初始化的const变量,都属于常量表达式所定义的范围。
// 24是常量表达式,24是一个字面值常量
// 单独使用sz是常量表达式,由字面值常量初始化
const int sz = 24;
// 注意:get_size()不是constexpr函数
// sz_1不是常量表达式,不是常量表达式初始化
const int sz_1 = get_size();
// sz_2是常量表达式,由常量表达式初始化
// sz + 2的值在编译阶段即可得出
const int sz_2 = sz + 2;
// sz_3不是常量表达式,虽然由字面值常量初始化
int sz_3 = 24;
在一个复杂的系统中,几乎不能分辨一个初始值到底是不是常量表达式。
虽然可以使用const
限定符修饰变量,表明它是常量的身份。但是在实际过程中,初始化变量值的很可能并非一个确定的值,即使用const
修饰的对象,并不一定是常量表达式。
int arr[get_size()];
// 此处要求get_size必须是一个
// 编译阶段就能确定的值,即常量表达式
此时可以使用constexpr
关键字,来告诉编译器我要定义的是一个常量表达式,且请帮我检查。
关于constexpr
关键字,有以下内容:
- constexpr修饰对象
- constexpr变量
- 字面值类型(一种归纳的定义)
- constexpr与引用
- constexpr与指针
- 字面值常量类
- constexpr修饰函数
1. constexpr修饰对象
这里使用关键词对象,而不是变量,意味着constexpr可以修饰【深度C++】之“类型与变量”中提到的某些类型,包括算数类型、指针、引用、一种特殊的类,请读者从“咬文嚼字”上予以理解。
1.1 constexpr变量
将一个变量声明为constexpr
意味着编译器将会验证变量的值是否是一个常量表达式,而且必须用常量表达式来初始化。
constexpr int mf = 20;
constexpr int limit = mf + 1;
// 当size是一个constexpr函数时,本条语句才可编译通过
constexpr int sz = size();
声明了constexpr,若编辑有误,将不会通过编译。这也是让代码更加鲁棒的一种方式。
constexpr
修饰变量和const
修饰变量的区别在于:
初始化const变量的可以不是一个constexpr,初始化constexpr的必须是constexpr
1.2 字面值类型(一种归纳的定义)
在【深度C++】之“类型与变量”中,我们按照基本内置类型、自定义类型和复合类型对C++支持的数据类型进行了分类。
在定义constexpr变量的时,它的值需要在编译时就得到,因此可以声明为constexpr的数据类型有所限制。这种类型一般比较简单,值显而易见、容易得到,因此统称为字面值类型(literal type)。
字面值类型包括算数类型、引用和指针,以及字面值常量类(见第1.5节)和枚举类型(参考【C++深陷】之“枚举”)。
constexpr修饰算数类型如1.1节所示。再次强调,初始化这样的变量的值必须是常量表达式。
1.3 constexpr与引用
引用是另一个对象的别名,定义一个引用必须为其初始化。
定义一个constexpr引用,只能绑定在全局变量和局部静态变量上(参考【深度C++】之“static”)。
// 全局变量
int PI = 3.1415926;
int main() {
// 局部静态变量
static int sz = 24;
// constexpr引用
constexpr int &r_PI = PI;
constexpr int &r_sz = sz;
return 0;
}
为什么呢?因为全局变量和局部静态变量,他们都有固定的地址!
1.4 constexpr与指针
指针的情况,比引用复杂一些。
constexpr指针要求它的值必须是nullptr
、0或者是存储于某个固定地址中的对象,也就是上文提到的全局变量和局部静态变量。
int PI = 3.1415926;
int main() {
// 局部静态变量
static int sz = 24;
// constexpr指针
constexpr int *p_PI = Π
constexpr int *p_sz = &sz;
constexpr int *p_null = nullptr;
constexpr int *p_0 = 0;
return 0;
}
指针是对象,在【深度C++】之“const”中,我们仔细探讨了顶层const与底层const,那么constexpr指针呢?
constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。即constexpr实现了顶层const的功能,定义了一个常量指针。
constexpr int PI = 3.1415926;
constexpr int sz = 24;
int a = 256;
int main() {
// p_PI是常量指针,只修饰顶层的指针
constexpr int *p_PI = Π
// p_sz是指向常量的常量指针
// 但此时必须指向全局变量或局部静态变量
constexpr const int *p_sz = &sz;
// 常量指针可以指向一个非常量
constexpr int *p_a = &a;
return 0;
}
这里也可以充分看到const
和constexpr
的区别:
// p_ca是指向常量的指针,不可以通过p_ca修改a
const int *p_ca = &a;
// p_a是常量指针,可以通过常量指针p_a修改a
constexpr int *cp_a = &a;
1.5 字面值常量类
字面值常量类定义于聚合类之上。
1.5.1 聚合类
当一个类满足如下条件是,它是一个聚合类:
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有virtual函数
struct Student {
int id;
unsigned short age;
};
// 可以这样初始化
Student sd = {100001, 22};
1.5.2 字面值常量类
数据成员都是字面值类型的聚合类,是字面值常量类。
如果一个类不是聚合类,它符合下述要求,也是一个字面值常量类:
- 数据成员都必须是字面值类型
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
- 类必须至少含有一个constexpr构造函数
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
定义的第一部分,是约束一个聚合类中的成员必须是字面值类型,不可以是string、vector这类。
定义的第二部分,我们从声明一个字面值常量类说起:①我们首先保证数据成员的字面值类型;②然后初始化数据成员必须使用常量表达式以及字面值常量类的constexpr构造函数,因为可以嵌套另一个字面值常量类;③其次给类自己定义一个constexpr构造函数;④最后析构函数使用默认定义。
这里提到了字面值类型,也就是之前归纳定义的算数类型、引用和指针,以及字面值常量类和枚举类型。
这里提到了constexpr构造函数的内容,请参考【深度C++】之“构造函数”。
2. constexpr修饰函数
2.1 基本用法
constexpr函数是指能用于常量表达式的函数。
定义一个constexpr函数遵循如下约定:
- 函数的所有形参及返回类型都是字面值类型
- 函数体只有一条return语句
constexpr int size() { return 42; }
constexpr int sz = size();
执行sz的初始化时,编译器把对constexpr函数的调用替换成其结果值。
这一点和inline修饰的函数有些类似,其实constexpr隐式地指定为内联函数。
constexpr函数体还可以包含空语句、类型别名以及using声明,他们都没有执行其他操作,因此可以写入constexpr函数体。
2.2 不一定会返回常量表达式
注意,constexpr函数不一定会返回常量表达式,和用法相关。因为constexpr函数并没有要求所有参数都是constexpr类型:
constexpr int size() { return 10; }
constexpr size_t scale(size_t cnt) { return size() * cnt; }
int arr[scale(2)];
// 编译通过,声明了一个大小为20的int类型数组
int scale_param = 3; // 这不是一个常量表达式
// 此时调用的是非constexpr函数的用法
// 因为初始化形参cnt的不是constexpr
int sz = scale(scale_param);
形如scale
这样声明的constexpr函数,实参是常量表达式,则返回值也是常量表达式;反之亦然。
其实引入常量表达式以及constexpr
才真正意义上定义了常量。以往的const
我们可以理解为一种Read Only。
而且在没有常量表达式以前,很多开销浪费在了函数调用上。假设我们封装了一些对枚举值的操作,这些操作肯定是调用频繁、函数体简单的。因为枚举是一种我们声明了就不会改变的自定义数据类型,如果没有常量表达式的概念,很多对枚举的操作,比如两个枚举值做位或、位与,都浪费在了函数调用上。有了常量表达式的概念后,通过编译器将函数展开,我们既可以维护逻辑清晰、低耦合性的代码,又可以保证了运行效率。
3. 总结
常量表达式(const expression) 是指值不会改变,并且在编译过程中就能得到计算结果的表达式。
通过使用关键字constexpr
可以声明一个常量对象。
为了方便总结和归纳,我们将算数类型、引用、指针、字面值常量类、枚举统称为字面值类型。可以且仅可以将字面值类型声明为constexpr表达式。
constexpr除了可以声明一个变量,还可以声明函数。编译过程中会在调用处展开。
使用constexpr可以极大的优化我们的代码。一是因为我们通常会将一些常量定义一个名字便于搜索、修改;二是我们取消某些函数调用的开销,却依旧保证逻辑清晰、低耦合性的代码。