author: hjjdebug
date: 2025年 05月 15日 星期四 16:03:33 CST
description: constexpr 关键字的意义(入门)
constexpr 是c++11 引入的一个关键字, 代表了一种属性.
文章目录
-
- [1. constexpr 修饰的变量, 在编译期间就可以得到其数值.](#1. constexpr 修饰的变量, 在编译期间就可以得到其数值.)
- [2. constexpr 修饰的函数, 可以在编译期间被调用.](#2. constexpr 修饰的函数, 可以在编译期间被调用.)
- [3. 研究代码](#3. 研究代码)
- [4. constexpr 修饰的变量, 可以调用constexpr 修饰的函数来赋值](#4. constexpr 修饰的变量, 可以调用constexpr 修饰的函数来赋值)
- [5. constexpr 修饰的函数怎样调试呢?](#5. constexpr 修饰的函数怎样调试呢?)
- [6. constexpr 修饰的函数, 在release版本下是怎样工作的?](#6. constexpr 修饰的函数, 在release版本下是怎样工作的?)
- [7. constexpr 还可以修饰构造函数,成员函数.](#7. constexpr 还可以修饰构造函数,成员函数.)
-
- [7.1 在常量表达式函数中你不能调用printf 函数](#7.1 在常量表达式函数中你不能调用printf 函数)
- [7.2 在运行函数中调用constexpr 对象](#7.2 在运行函数中调用constexpr 对象)
- [8 小结:](#8 小结:)
1. constexpr 修饰的变量, 在编译期间就可以得到其数值.
例如: constexpr int arr_ce[5]={1,2,3,4,5};
这里编译器gcc 会给arr_ce 变量加上 constexpr 属性.
这里你会说,我不加constexpr 不是一样声明变量并赋初值啊.
int arr_ce[5]={1,2,3,4,5};
是的, 这样也声明并初始化了变量arr_ce[5], 但它们没有constexpr属性,
那constexpr 属性有什么用? 且看后面慢慢道来.
2. constexpr 修饰的函数, 可以在编译期间被调用.
例如: 费伯纳兹函数F, 我们加上constexpr 修饰词.
constexpr int F(int n) {
return n == 0 ? 0 :
(n == 1 ? 1: F(n-1)+F(n-2));
}
有了这个属性和没有这个属性有什么差别呢?
答案来了,有这个属性的构成一个群! 构成一个在编译时就可以执行的群.
下面举例说明.
3. 研究代码
cpp
$ cat main.cpp
#include <stdio.h>
//constexpr 修饰的函数,可以在编译期计算.
constexpr int F(int n) {
return n == 0 ? 0 :
(n == 1 ? 1: F(n-1)+F(n-2));
}
//constexpr 修饰的变量,可以调用constexpr 修饰的函数来赋值
//直接得到常量数据
constexpr int arr_ce[5]={
F(0),F(1),F(2),F(3),F(4)
};
int arr_n[5];
struct Point {
//在常量表达式函数中你不能调用printf 函数
constexpr Point(int x, int y) : x(x), y(y) {/*printf("Point construct\n");*/}
int x, y;
};
constexpr Point p1(1, 2); // ✅ 合法:在编译期间就可以构建对象,对象地址在全局数据区并构建完成
int main() {
printf("%d %d %d %d %d\n",arr_ce[0],arr_ce[1],arr_ce[2],arr_ce[3],arr_ce[4]);
//constexpr 修饰的函数,也可以在运行期工作. 如下:
//在debug版本下, 你可以单步调试程序.
//在release版本下, 函数名称及函数结构被优化掉了.
//我以为它会优秀的把计算结果直接填进去,
//实际上也不是 ,还是有计算的过程.
//它把计算代码穿插到了调用代码中.
unsigned long int i;
for(i=0;i<5;i++)
{ //注意这里,release 版直接把函数代码穿插进来
arr_n[i]=F(i);
}
constexpr Point p2(3, 4); // 放到这里用constexpr 修饰就有点不伦不类,也可以这样做.
printf("p1:%p,p2:%p\n",&p1,&p2);
return 0;
}
//constexpr 还可以修饰构造函数,成员函数. 意味着这些函数在编译期间就可以调用.
4. constexpr 修饰的变量, 可以调用constexpr 修饰的函数来赋值
//直接得到常量数据
constexpr int arr_ce[5]={
F(0),F(1),F(2),F(3),F(4)
};
如果没有constexpr 来修饰F(), 这样的书写方式是不允许的.
error: call to non-'constexpr' function 'int F(int)'
我们看一下arr_ce[5]的数值
其对应的反汇编代码.
printf("%d %d %d %d %d\n",arr_ce[0],arr_ce[1],arr_ce[2],arr_ce[3],arr_ce[4]);
0x0000555555555155 <+12>: mov $0x3,%edi
0x000055555555515a <+17>: mov $0x2,%esi
0x000055555555515f <+22>: mov $0x1,%ecx
0x0000555555555164 <+27>: mov $0x1,%edx
0x0000555555555169 <+32>: mov $0x0,%eax
0x000055555555516e <+37>: mov %edi,%r9d
0x0000555555555171 <+40>: mov %esi,%r8d
0x0000555555555174 <+43>: mov %eax,%esi
0x0000555555555176 <+45>: lea 0xea7(%rip),%rdi # 0x555555556024
0x000055555555517d <+52>: mov $0x0,%eax
0x0000555555555182 <+57>: callq 0x555555555050 printf@plt
arr_ce[] 的5个数据是 0 1 1 2 3
我们看到printf 的前6个参数
rdi -> 字符串地址
rsi -> 0
rdx -> 1
rcx -> 1
r8 -> 2
r9 -> 3
说明打印时直接传递了数据.
忘了,原来直接看数组的内容才更简洁.
(gdb) p arr_ce
$1 = {0, 1, 1, 2, 3}
5. constexpr 修饰的函数怎样调试呢?
constexpr 函数也是我们自己写的, 那么多代码,运行出错了怎么办?
constexpr 修饰的函数,也可以在运行期工作. 例如我们在main函数中调用它,
把它的数值付给一个非constexpr 的数据, debug 版本下可以跟入函数来调试.
不过constexpr 修饰的函数,按说不应该在运行期调用, 否则你不如去掉constexpr 修饰.
6. constexpr 修饰的函数, 在release版本下是怎样工作的?
这应该属于进阶分析了. 因为constexpr 修饰的函数,不建议在运行期调用,
但我调用了,会是什么情况呢?更何况谁会保证不会穿插调用呢.
我试了一下,调用没问题, 但奇怪的是release 版本下怎么看不到 被调用的函数名呢?
是的,你用nm 工具确实查不到constexpr 修饰的函数, 我用反汇编分析了一下,发现gcc把
constexpr 修饰的函数直接优化穿插到调用代码中来了,gcc很神奇, 看下边汇编分析
cpp
int arr[5];
int main() {
unsigned long int i;
for(i=0;i<sizeof(arr)/sizeof(arr[0]);i++)
{ //注意这里, release 版直接把代码穿插进来,没有对应的F()代码.
arr[i]=F(i);
}
return 0;
}
0000000000001040 <main>:
1040: f3 0f 1e fa endbr64
1044: 41 55 push %r13
1046: 31 c0 xor %eax,%eax
1048: 41 54 push %r12
104a: 45 31 e4 xor %r12d,%r12d
104d: 55 push %rbp
104e: 53 push %rbx
104f: 31 db xor %ebx,%ebx
1051: 48 83 ec 08 sub $0x8,%rsp //调整堆栈,开辟局部空间
1055: 4c 8d 2d c4 2f 00 00 lea 0x2fc4(%rip),%r13 # 4020 <arr>
105c: 0f 1f 40 00 nopl 0x0(%rax)
1060: 44 01 e0 add %r12d,%eax // F(n-1) + F(n-2)
1063: 41 89 44 9d 00 mov %eax,0x0(%r13,%rbx,4) //保留ebx 到 arr[i]
1068: 48 83 c3 01 add $0x1,%rbx
106c: 48 83 fb 05 cmp $0x5,%rbx //i==5
1070: 74 2e je 10a0 <main+0x60> //退出循环
1072: 89 dd mov %ebx,%ebp
1074: 45 31 e4 xor %r12d,%r12d
1077: b8 01 00 00 00 mov $0x1,%eax
107c: 48 83 fb 01 cmp $0x1,%rbx // i==1?
1080: 74 de je 1060 <main+0x20> //进行处理
1082: 8d 7d ff lea -0x1(%rbp),%edi
1085: e8 16 01 00 00 callq 11a0 <_Z7Fabnacii> //递归恢复堆栈
108a: 41 01 c4 add %eax,%r12d //r12d 为F(n-1)
108d: 89 e8 mov %ebp,%eax //ebp 是保留的i
108f: 83 e8 02 sub $0x2,%eax // i==2 ?
1092: 74 cc je 1060 <main+0x20> //进行处理
1094: bd 02 00 00 00 mov $0x2,%ebp
1099: 83 f8 01 cmp $0x1,%eax // i==3 ?时
109c: 75 e4 jne 1082 <main+0x42> // i==其它时的处理
109e: eb c0 jmp 1060 <main+0x20> //进行处理
10a0: 48 83 c4 08 add $0x8,%rsp //恢复堆栈
10a4: 31 c0 xor %eax,%eax //返回0
10a6: 5b pop %rbx //恢复保留的寄存器
10a7: 5d pop %rbp
10a8: 41 5c pop %r12
10aa: 41 5d pop %r13
10ac: c3 retq
10ad: 0f 1f 00 nopl (%rax)
7. constexpr 还可以修饰构造函数,成员函数.
意味着这些函数在编译期间就可以调用.
7.1 在常量表达式函数中你不能调用printf 函数
struct Point {
constexpr Point(int x, int y) : x(x), y(y) {printf("Point construct\n");}
int x, y;
};
实例化结构变量时会有错误. 说printf 不是常量表达式.
error: 'printf(((const char*)"Point construct\012"))' is not a constant expression
常量表达式是在编译时就能确定的表达式. printf 是运行时的调用函数,其执行由调用参数及系统环境来确定
所以常量表达式函数中不能调用 printf 函数.
7.2 在运行函数中调用constexpr 对象
// 把constexpr 函数放到main() 下执行也是可以的,尽管觉得有点不伦不类.
35 constexpr Point p2(3, 4);
0x00000000004011d2 <+124>: movl $0x3,-0x10(%rbp)
0x00000000004011d9 <+131>: movl $0x4,-0xc(%rbp)
此是gcc 把代码编译成 对象初始化参数直接存入对象的成员地址. 很简明, 没有所谓的显示调用构造函数的过程.
与release版调用constexpr 函数优化穿插代码类似.
8 小结:
算入门介绍吧. 介绍了constexpr 修饰全局变量(当然局部变量也可以),修饰函数,修饰成员函数,修饰构造函数的
使用场景,并给出了实例和实现分析. 原来其好处是gcc 在生成运行代码时,先自己一通运算把能确定下来的东西先确定下来.
后面还有针对模板的使用下期介绍.