现代C++实践100练:吃透C++新特性constexpr

1,028 阅读6分钟

在讲这个问题之前,我想先说下本文的风格,这也是我写作的风格,通常我不会像教科书一般上来就直说概念和道理,我喜欢用已知,大家可以理解的问题去引入一个知识,这是我自己的学习方法,也希望介绍给大家。

例如本文,很多人可能不知道constexpr,也不知道为什么要引入它,但是大家都知道const,通过const引入,就往往容易接受的多。

本文你可以了解到什么

了解constexpr这一优秀的新特性

你可以知道我们为什么建议使用常量constexpr,它比const优秀?

const

const是一个C语言的关键字,它限定一个变量不允许被改变。

在之前const就是被作为常量使用的,现在多了一个constexpr是不是多此一举呢?

我们继续来看,了解一下const的作用:

(1)可以定义const常量,具有不可变性。   

例如:const int Max=100; Max++会产生错误;  

(2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。   

例如: void f(const int i) { .........} 编译器就会知道i是一个常量,不允许修改;

(3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。 同宏定义一样,可以做到不变则已,一变都变!

(4)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。

(5) 可以节省空间,避免不必要的内存分配。

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干份拷贝。

从上面几个地方看const就是常量,那么我们进行一个小测试:

const int len = 5;
​
int a[len];

可能在现在编译器使用,它都是不报错的,因为已经经过了编译器优化,但是在老版本的编译器中往往会报错!

这是因为 C++ 标准中数组的长度必须是一个常量表达式,而对 于 len而言,这是一个 const 常数,而不是一个常量表达式,因此(即便这种行为在大部分编译器中 都支持,但是)它是一个非法的行为。

如果你想知道,这到底是为什么,可以看下这段我的总结,但是我不保证它百分百正确!

数组的长度需要常量定义,像是5、6、7这种肯定是常量,只读;因为常量是被编译器放在内存中的只读区域,当然也就不能够去修改它。

而const变量实在内存中存在的,只不过编译器不允许它被修改,毕竟const定义的变量,虽然被叫做常量,但是更细一点是被叫做”常值变量“,当作变量看待。

constexpr:常量表达式

constexpr(常量表达式):是指值不会改变并且在编译过程就能得到计算结果的表达式。

常量表达式的优点是将计算过程转移到编译时期,那么运行期就不再需要计算了,程序性能也就提升了。

const好用,但是在某些情况下,我们还是会被它所谓的”常量“,给迷惑,产生错误用法,那既然如此,到底有没有真正的常量定义呢!

答案:有的。

C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这 个关键字明确的告诉编译器应该去验证 constexpr定义的值在编译期就应该是一个常量表达式。

constexpr int len = 5;
​
int a[len];

此时使用合法!

constexpr定义函数

constexpr int Length_Constexpr()
{
    return 5;
}
​
char arr_2[Length_Constexpr() + 1]; // 合法

constexpr返回值也是常量!

但是constexpr函数和正常函数肯定是不一样的,因为它需要在编译期做事,需要有一定的使用限制!

从C++11开始,constexpr函数不仅可以返回常量,还可以进行递归操作。

constexpr int fibonacci(const int n) 
{
return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}

从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句。

constexpr int fibonacci(const int n) 
{
if(n == 1) return 1;
if(n == 2) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}

编译期的优化

C++ 本身已经具备了常量表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没 有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。

性能实测

​
constexpr int Calculate1 (int x, int y)
{
    return x * y * 32 / 14;
}
int Calculate2 (int x, int y)
{
    return x * y * 32 / 14;
}
int main()
{
    uint64_t start, end;
    start = GetMicroSeconds();
    for(int i = 0;i < 100000; i++)
{
//也可以换成const修饰,也是常量表达式
        constexpr int ret = Calculate1 (11, 12); 
    }
    end = GetMicroSeconds();
    cout<<"spend time: "<<end-start<<" us"<<endl;
       
    start = GetMicroSeconds();
    for(int i = 0;i < 100000; i++)
    {
//不能用constexpr修饰,不是常量表达式,会编译报错
        int ret = Calculate2 (11, 12);
    }
    end = GetMicroSeconds();
    cout<<"spend time: "<<end-start<<" us"<<endl;
    return 0;
}

这段代码比较简单,一个常量表达式和一个非常量表达式,都进行相同的计算,都循环10w次,然后记录各自的总耗时,单位是微秒。

打印结果如下:

spend time: 182 us
spend time: 929 us

大家可以看到,接近5倍的性能差别,这要是发生在高性能开发中,将是一次不错的性能提升。

使用总结

语义上来说:

如果变量用constexpr修饰,那么变量也具有const的特性;如果变量用const修饰,不能说明变量具有constexpr的特性。从语义上讲,const更像是“read only”,而constexpr更像是“const”。

一般来说,当你认为变量一定是常量表达式,那就把它声明成constexpr类型吧。

参考资料

现代C++之constexpr

const和constexpr还在傻傻分不清?

\