constexpr 是 C++11 中增加的关键字,我们可以使用该关键字定义以下对象:
- 编译期常量;
- 常量表达式函数;
- 编译期常量对象。
以下代码运行环境为:win10 专业版 + vs2019 社区版。
1. 编译期常量
常量分为编译期常量、运行期常量。
编译期常量指的是在编译阶段就可以确定其值,并将其结果展开到使用的地方,不占用内存空间。
运行期常量本质上是只读的变量,需要占用内存空间,编译时无法确定其值,运行时,无法修改其值。
constexpr 关键字就是用来定义编译期的常量。
有些同学可能想,constexpr 和 const 有什么区别呢?
constexpr 只能定义编译期常量,而 const 可以定义编译期常量,也可以定义运行期常量。
请看下面 const 的代码:
void test()
{
const int a = 10;
int b = 20;
const int c = b;
}
第 3 行定义一个编译期常量。
第 6 行定义了一个运行期常量。这是因为 b 是一个变量,编译期无法确定其值,必须到运行期才能确定。而 c 的值必须等到 b 确定才能确定,所以,c 是一个运行期常量。
上面第 6 行代码中的常量定义如果将 const 换成 constexpr ,则代码就会报语法错误,因为 constexpr 只能定义编译期常量。
void test()
{
const int a = 10;
int b = 20;
constexpr int c = b;
}
上述代码编译报错如下:
说到常量有些同学可能不太清楚,程序中为什么要使用常量呢?有两点原因如下:
- 程序中总是需要一些不能修改的数据,写成变量则就有被意外修改的风险。
- 常量可以在编译期确定其值,某些场景下可以将一些计算任务放在编译阶段,从而提升程序效率。
2. 常量表达式函数
什么是常量表达式函数呢?请看下面的普通函数:
int my_sum(int n)
{
if (n == 1)
{
return 1;
}
return n + my_sum(n - 1);
}
普通函数必须在运行时才能执行,进而计算出结果。而常量表达式函数要求函数在编译期就计算出结果,运行时直接使用结果。也就是说将函数的执行从运行阶段转移到编译阶段,提升程序运行效率。
将上面的 my_sum 函数定义为常量表达式函数非常简单,只需要在函数返回值类型前面加上 constexpr 关键字即可。示例代码如下:
constexpr int my_sum(int n)
{
if (n == 1)
{
return 1;
}
return n + my_sum(n - 1);
}
为了能够让 my_sum 函数在编译阶段计算出结果,我们必须使用 constexpr 编译期常量来保存常量表达式函数的结果,否则 my_sum 函数仍然会在运行期执行。如下代码所示:
constexpr int my_sum(int n)
{
if (n == 1)
{
return 1;
}
return n + my_sum(n - 1);
}
void test()
{
constexpr int a = my_sum(3);
}
我们在 VS 2019 中查看第 13 行代码对生成的汇编代码如下:
mov dword ptr [a], 6
第 13 行代码只对应一行汇编指令,数字 6 则是编译阶段执行 my_sum(3) 的计算结果。
如果将 constexpr 去掉,则第 13 行对应的汇编代码如下:
push 3
call my_sum (03913B1h)
add esp,4
mov dword ptr [a],eax
从汇编指令的条数来看,常量表达式函数明显较少。并且,运行期的函数计算每次都需要调用 my_sum, 而常量表达式则不是,运行效率更高。
3. 编译期常量对象
编译期常量对象的任何计算都在编译期完成。定义编译期常量对象,有以下几点要求:
- 构造函数使用 constexpr 修饰,必须使用初始化列表对成员进行初始化。
- 对象调用的成员函数必须使用 constexpr 修饰。请看下面的示例代码:
class Box
{
public:
constexpr Box(int l, int w, int h) : m_l(l), m_w(w), m_h(h) {}
constexpr int get_volume() const
{
return m_l * m_w * m_h;
}
int get_sum() const
{
return (m_l + m_w + m_h);
}
public:
int m_l;
int m_w;
int m_h;
};
void test()
{
constexpr Box box(10, 20, 30);
constexpr int volume = box.get_volume();
int my_sum = box.get_sum();
}
第 4 行 Box 的构造函数使用 constexptr 修饰,并且使用初始化列表对成员进行初始化。这就保证了对象成员 m_l、m_w、m_h 在编译期确定其值。
第 5 行 get_volume 函数也被定义为了常量表达式成员函数,调用该函数则会在编译阶段计算出结果。
第 10 行 get_sum 函数则是普通函数,该函数必须声明为 const 函数,否则无法被常量对象调用。
第 23 行 Box 类型的对象 box 必须使用 constexpr 来修饰,使得其能够调用常量表达式构造函数。该行代码对应的汇编代码如下:
constexpr Box box(10, 20, 30);
mov dword ptr [box], 0Ah
mov dword ptr [ebp-10h], 14h
mov dword ptr [ebp-0Ch], 1Eh
其中 0Ah 为十六进制,对应十进制为 10,14h 对应十进制为 20, 1Eh 对应十进制为 30, 三行汇编代码的作用就是将 10、20、30 赋值给成员变量 m_l、m_w、m_h。注意:此处并没有调用 Box 构造函数。
第 24 行通过常量对象 box 调用常量表达式成员函数,使得在编译期计算出函数的执行结果,并赋值给编译期常量 volume。该行代码对应的汇编代码如下:
constexpr int volume = box.get_volume();
mov dword ptr [volume], 1770h
其中, 十六进制 1770h 对应的十进制为 6000,该行代码的作用是将值赋值 6000 给常量 volume。函数 get_volume 已经在编译期执行完毕,运行期直接使用计算结果 6000。
第 25 行通过常量对象调用了普通函数 show,该函数会在运行期进行执行。该行代码对应的汇编代码如下:
int my_sum = box.get_sum();
lea ecx, [box]
call Box::get_sum (0FD1131h)
mov dword ptr [my_sum],eax
call 指令表示调用后面的函数。我们可以看到,运行期调用了 get_sum 函数计算结果,而不是直接使用结果。
至此,关于 constexpt 讲解完毕,希望对你有所帮助!