Last Updated: 2023-12-25 07:47:28 Monday
-- TOC --
看到constexpr,就应该理解意识到,这个expression必须可以在编译期被执行。C++给我们带来了比conditional inclusion更强大的编译期编程。
有constexpr(constant expression)
这个prefix修饰的函数接口,这样的接口必须可以在编译时被执行。
Constant expressions are expressions that must could be evaluated at compile time. For performance and safety reasons, whenever a computation can be done at compile time rather than runtime, you should do it. Simple mathematical operations involving literals are an obvious example of expressions that can be evaluated at compile time.
用constexpr修饰的函数,也完全可以在runtime被调用,当编译器发现某个调用处,无法在编译期处理的时候,就会在代码中保留此函数,使得runtime时可以被调用。
在C语言中,我们常常用#define的方式,来定义一些常量。在定义时,有时也会将常量的计算方法写出来。虽然编译器也会在编译时,将这些常量计算出来,直接用在生成汇编指令中,但这种方式存在一些坑,容易出错,比如下面这个错误:
#define ABC 1024*8 // wrong
a = (b+c) % ABC; // (b+c) & 1024 * 8 !!
// #define ABC (1024*8) // right
C++更近了一步,通过使用constexpr申明,可以用函数接口的方式,在编译时,计算这样的常量。(constexpr在编译期间起作用)
constexpr带来的好处是:
请看下面的测试代码:
#include <cstdio>
// return int square root
constexpr int isqrt(int n) {
int i=1;
while (i*i<n) ++i;
return i-(i*i!=n);
}
int main() {
int x = isqrt(1764);
int y = isqrt(255);
printf("%d %d\n", x, y);
return 0;
}
constexpr int isqrt(int n)
这个接口返回输入值的最接近的整数平方根。
运行结果:
$ g++ -Wall -Wextra test2.cpp -o test2
$ ./test2
42 15
我们来看一下编译后的汇编:
$ g++ -c test2.cpp
$ objdump -d test2.o
test2.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 2a 00 00 00 movl $0x2a,-0x4(%rbp) # 0x2a = 42
f: c7 45 f8 0f 00 00 00 movl $0xf,-0x8(%rbp) # 0xf = 15
16: 8b 55 f8 mov -0x8(%rbp),%edx
19: 8b 45 fc mov -0x4(%rbp),%eax
1c: 89 c6 mov %eax,%esi
1e: bf 00 00 00 00 mov $0x0,%edi
23: b8 00 00 00 00 mov $0x0,%eax
28: e8 00 00 00 00 call 2d <main+0x2d>
2d: b8 00 00 00 00 mov $0x0,%eax
32: c9 leave
33: c3 ret
从编译后的汇编来看,isqrt这个用constexpr申明的接口,在编译的时候,就执行完毕,得到了结果,并将结果直接写入生成的机器码中。
汇编学习资料:x86和x64汇编基础
这个编译结果的另一个重要细节是,isqrt这个函数的机器码,完全没有出现在最后生成的object文件中,因为不需要。
一个用constexpr
申明的接口,也可以传入在编译时无法确定值的变量,此时就是运行时的函数调用。我们修改一下测试代码,再看看汇编的情况:
#include <cstdio>
#include <cstdlib>
#include <cassert>
// return int square root
constexpr int isqrt(int n) {
int i=1;
while (i*i<n) ++i;
return i-(i*i!=n);
}
int main(int argc, char **argv) {
assert(argc == 2);
int x = isqrt(1764);
int y = isqrt(255);
printf("%d %d\n", x, y);
printf("%d\n", isqrt(atoi(argv[1])));
return 0;
}
上面的测试代码,最后一次调用isqrt,传入的参数在编译时不能确定,是运行时传入的。此时,代码的汇编情况如下:
$ g++ -c test2.cpp
[xinlin@fedora private_test]$ objdump -d test2.o
test2.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 20 sub $0x20,%rsp
8: 89 7d ec mov %edi,-0x14(%rbp)
b: 48 89 75 e0 mov %rsi,-0x20(%rbp)
f: 83 7d ec 02 cmpl $0x2,-0x14(%rbp)
13: 74 19 je 2e <main+0x2e>
15: b9 00 00 00 00 mov $0x0,%ecx
1a: ba 0f 00 00 00 mov $0xf,%edx
1f: be 00 00 00 00 mov $0x0,%esi
24: bf 00 00 00 00 mov $0x0,%edi
29: e8 00 00 00 00 call 2e <main+0x2e>
2e: c7 45 fc 2a 00 00 00 movl $0x2a,-0x4(%rbp)
35: c7 45 f8 0f 00 00 00 movl $0xf,-0x8(%rbp)
3c: 8b 55 f8 mov -0x8(%rbp),%edx
3f: 8b 45 fc mov -0x4(%rbp),%eax
42: 89 c6 mov %eax,%esi
44: bf 00 00 00 00 mov $0x0,%edi
49: b8 00 00 00 00 mov $0x0,%eax
4e: e8 00 00 00 00 call 53 <main+0x53>
53: 48 8b 45 e0 mov -0x20(%rbp),%rax
57: 48 83 c0 08 add $0x8,%rax
5b: 48 8b 00 mov (%rax),%rax
5e: 48 89 c7 mov %rax,%rdi
61: e8 00 00 00 00 call 66 <main+0x66>
66: 89 c7 mov %eax,%edi
68: e8 00 00 00 00 call 6d <main+0x6d>
6d: 89 c6 mov %eax,%esi
6f: bf 00 00 00 00 mov $0x0,%edi
74: b8 00 00 00 00 mov $0x0,%eax
79: e8 00 00 00 00 call 7e <main+0x7e>
7e: b8 00 00 00 00 mov $0x0,%eax
83: c9 leave
84: c3 ret
Disassembly of section .text._Z5isqrti:
0000000000000000 <_Z5isqrti>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d ec mov %edi,-0x14(%rbp)
7: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
e: eb 04 jmp 14 <_Z5isqrti+0x14>
10: 83 45 fc 01 addl $0x1,-0x4(%rbp)
14: 8b 45 fc mov -0x4(%rbp),%eax
17: 0f af c0 imul %eax,%eax
1a: 39 45 ec cmp %eax,-0x14(%rbp)
1d: 7f f1 jg 10 <_Z5isqrti+0x10>
1f: 8b 45 fc mov -0x4(%rbp),%eax
22: 0f af c0 imul %eax,%eax
25: 39 45 ec cmp %eax,-0x14(%rbp)
28: 0f 95 c0 setne %al
2b: 0f b6 c0 movzbl %al,%eax
2e: 8b 55 fc mov -0x4(%rbp),%edx
31: 29 c2 sub %eax,%edx
33: 89 d0 mov %edx,%eax
35: 5d pop %rbp
36: c3 ret
首先发现,isqrt函数接口的汇编出现在了object文件中,说明需要用到它了。
其次,仔细看汇编代码,还是能够发现0x2a
和0xf
这两个常量。
以上代码执行结果:
$ g++ -Wall -Wextra test2.cpp -o test2
$ ./test2 12345
42 15
111
其它细节不太明显,我们试一下反编译可执行程序,下面值显示main和isqrt函数的反汇编结果:
$ objdump -d test2
...
0000000000401146 <main>:
401146: 55 push %rbp
401147: 48 89 e5 mov %rsp,%rbp
40114a: 48 83 ec 20 sub $0x20,%rsp
40114e: 89 7d ec mov %edi,-0x14(%rbp)
401151: 48 89 75 e0 mov %rsi,-0x20(%rbp)
401155: 83 7d ec 02 cmpl $0x2,-0x14(%rbp)
401159: 74 19 je 401174 <main+0x2e>
40115b: b9 10 20 40 00 mov $0x402010,%ecx
401160: ba 0f 00 00 00 mov $0xf,%edx
401165: be 26 20 40 00 mov $0x402026,%esi
40116a: bf 30 20 40 00 mov $0x402030,%edi
40116f: e8 cc fe ff ff call 401040 <__assert_fail@plt>
401174: c7 45 fc 2a 00 00 00 movl $0x2a,-0x4(%rbp)
40117b: c7 45 f8 0f 00 00 00 movl $0xf,-0x8(%rbp)
401182: 8b 55 f8 mov -0x8(%rbp),%edx
401185: 8b 45 fc mov -0x4(%rbp),%eax
401188: 89 c6 mov %eax,%esi
40118a: bf 3a 20 40 00 mov $0x40203a,%edi
40118f: b8 00 00 00 00 mov $0x0,%eax
401194: e8 97 fe ff ff call 401030 <printf@plt>
401199: 48 8b 45 e0 mov -0x20(%rbp),%rax
40119d: 48 83 c0 08 add $0x8,%rax
4011a1: 48 8b 00 mov (%rax),%rax
4011a4: 48 89 c7 mov %rax,%rdi
4011a7: e8 a4 fe ff ff call 401050 <atoi@plt>
4011ac: 89 c7 mov %eax,%edi
4011ae: e8 18 00 00 00 call 4011cb <_Z5isqrti> # call isqrt
4011b3: 89 c6 mov %eax,%esi
4011b5: bf 41 20 40 00 mov $0x402041,%edi
4011ba: b8 00 00 00 00 mov $0x0,%eax
4011bf: e8 6c fe ff ff call 401030 <printf@plt>
4011c4: b8 00 00 00 00 mov $0x0,%eax
4011c9: c9 leave
4011ca: c3 ret
00000000004011cb <_Z5isqrti>:
4011cb: 55 push %rbp
4011cc: 48 89 e5 mov %rsp,%rbp
4011cf: 89 7d ec mov %edi,-0x14(%rbp)
4011d2: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
4011d9: eb 04 jmp 4011df <_Z5isqrti+0x14>
4011db: 83 45 fc 01 addl $0x1,-0x4(%rbp)
4011df: 8b 45 fc mov -0x4(%rbp),%eax
4011e2: 0f af c0 imul %eax,%eax
4011e5: 39 45 ec cmp %eax,-0x14(%rbp)
4011e8: 7f f1 jg 4011db <_Z5isqrti+0x10>
4011ea: 8b 45 fc mov -0x4(%rbp),%eax
4011ed: 0f af c0 imul %eax,%eax
4011f0: 39 45 ec cmp %eax,-0x14(%rbp)
4011f3: 0f 95 c0 setne %al
4011f6: 0f b6 c0 movzbl %al,%eax
4011f9: 8b 55 fc mov -0x4(%rbp),%edx
4011fc: 29 c2 sub %eax,%edx
4011fe: 89 d0 mov %edx,%eax
401200: 5d pop %rbp
401201: c3 ret
...
可以看到对isqrt的调用,只有1次!由于是C++代码,编译器使用了符号修饰。
现在可以总结:constexpr是个好东西,灵活高效安全。
In certain contexts, like embedded development, constexpr is indispensable. In general, if an expression can be declared constexpr, you should strongly consider doing so. Using constexpr rather than manually calculated literals can make your code more expressive. Often, it can also seriously boost performance and safety at runtime.
constexpr除了用来申明函数接口,还可以直接定义常量:
#include <cstdio>
constexpr unsigned int usi = 12345;
constexpr float abc = 1.2345f;
constexpr float gg = usi * abc; // calucale gg in compile time
int main(void){
printf("%u * %f = %f\n", usi, abc, gg);
return 0;
}
这在使用上,依然比#define定义宏替换要更安全。
从我看到的资料来理解:C++中的constexpr修饰,随着C++版本的推进,也变得越来越强大,越来越多的计算被编译器考虑纳入编译期完成!
C++20把对constexpr的限制又放宽很多了,可以 new delete try catch 了,虚函数析构函数也能是 constexpr 了...
下面摘一段《A Tour of C++》的原文:
const
: meaning roughlyI promise not to change this value.
This is used primarily to specify interfaces so that data can be passed to functions using pointers and references without fear of it being modified. The compiler enforces the promise made by const. The value of a const can be calculated at run time.
constexpr
: meaning roughlyto be evaluated at compile time.
This is used primarily to specify constants, to allow placement of data in read-only memory (where it is unlikely to be corrupted), and for performance. The value of a constexpr must be calculated by the compiler.
constexpr if
让以前本应被写在一起,却在C++17前没法写在一起的代码得到了改善。
如果我们写一个支持多种类型的输入和输出的toStr接口,在C++17之前,要这样:
#include <iostream>
#include <string>
using namespace std;
template<typename T>
constexpr enable_if_t<is_same_v<T,string>,string> toStr(T t){
return t;
}
template<typename T>
constexpr enable_if_t<!is_same_v<T,string>,string> toStr(T t){
return to_string(t);
}
int main(){
int a {123};
string b {"abc"};
double c {1.234};
cout << toStr(a) << endl;
cout << toStr(b) << endl;
cout << toStr(c) << endl;
return 0;
}
如果将上面两个toStr定义合并在一个template中,使用if判断,会导致编译错误。因为用string类型实例化模板之后,虽然存在一个死分支,但编译器不管,死分支也会编译,此时给to_string传入string对象,导致编译错误。
enable_if_t是C++ SFINAE(Substitution Failure Is Not An Error)的应用。在编译期,enable_if_t只有在第一个参数为true的时候,才存在,当它不存在的时候,就是一个substitution failure,此时编译器就会去选择其它的匹配模板。
从C++17开始,上面的代码可以写成这样:
template<typename T>
constexpr auto toStr(T t){
if constexpr(is_same_v<T,string>)
return t;
else
return to_string(t);
}
在实例化模板接口的时候,编译器在面对if constexpr的时候,只会实例化那个为true的分支,解决上之前C++版本一定要分开写的问题,同时也让实例化后的代码size变小了。
下面是经典的编译期sum接口:
#include <iostream>
#include <string>
using namespace std;
/*template<typename T>
constexpr T sum(T t){
return t;
}
template<typename T, typename... Ts>
constexpr T sum(T t, Ts... args){
return t + sum(args...);
}*/
// C++17
template<typename T, typename... Ts>
constexpr T sum(T t, Ts... args){
if constexpr(sizeof...(args) == 0)
return t;
else
return t + sum(args...);
}
int main(){
cout << sum(1) << endl;
cout << sum(1,2,3) << endl;
cout << sum(1.0,2.0,3.0) << endl;
cout << sum(1.0,2,3) << endl;
return 0;
}
sizeof...(args)
是variadic template参数特有的,用来获得参数的个数。
从C++17开始,lambda函数接口也可以被声明为constexpr了。也就是说,他们可以被用在任何constexpr的上下文中。同样的,对一个lambda而言,只要被捕获的变量是字面量类型(lieteral type),那么整个lambda也将表现为字面量类型。
#include <iostream>
#include <string>
using namespace std;
template<typename T>
constexpr auto addTo(T t){
return [t](T i){return t+i;};
}
int main(){
constexpr auto add8 { addTo(8) };
cout << add8(1) << endl;
constexpr auto add8f { addTo(8.0) };
cout << add8f(2.1) << endl;
return 0;
}
本文链接:https://cs.pynote.net/sf/c/cpp/202208181/
-- EOF --
-- MORE --