解读C++ constexpr关键字的特性

Posted 泡沫o0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解读C++ constexpr关键字的特性相关的知识,希望对你有一定的参考价值。

使用 constexpr 的最佳实践:优化性能和减少错误

1. 引言

a. C++11中引入的constexpr关键字

在C++11中,引入了一个新的关键字:constexpr。这个关键字允许开发者将变量、函数和表达式标记为在编译时确定其值的常量,从而使编译器在编译期间进行计算。这有助于提高代码性能,减少运行时开销,并为编译时验证和元编程提供支持。

b. 编译时计算与运行时计算的区别

编译时计算是在编译阶段进行的计算,其结果在程序运行前就已经确定。运行时计算是在程序执行时进行的计算,其结果取决于程序的输入和状态。通过将一些可以在编译时确定的计算提前至编译阶段,可以减少程序的运行时开销,提高性能。

c. constexpr的意义和优势

使用constexpr关键字的主要优势如下:

提高代码性能:通过将计算转移到编译阶段,可以减少运行时的计算开销。
编译时验证:可以在编译阶段对一些值进行验证,确保代码的正确性。
支持元编程:可以利用constexpr进行编译时的计算,实现高效的元编程技术。


2. constexpr基本语法

a. constexpr变量

i. 语法

使用constexpr关键字定义变量时,变量的值必须在编译时确定:

constexpr int a = 10;
constexpr double b = 3.14;

ii. 使用场景

constexpr变量适用于那些在编译时已知的值,例如数学常量、配置参数等。这些值在程序运行期间不会发生变化,因此将它们标记为constexpr有助于提高代码的性能和可读性。

b. constexpr函数

i. 语法

使用constexpr关键字定义函数时,函数的返回值必须在编译时确定:

constexpr int square(int x) 
    return x * x;

ii. 使用场景

constexpr函数适用于那些可以在编译时计算出结果的函数。例如,一些数学运算、编译时数据生成等。通过将这些函数标记为constexpr,可以实现在编译时进行计算,提高代码的性能。

c. constexpr条件表达式

在constexpr函数中,可以使用条件表达式(如if和switch)进行编译时的条件判断:

constexpr int factorial(int n) 
    return n <= 1 ? 1 : n * factorial(n - 1);

d. 常量表达式和常量初始化器

使用constexpr关键字定义的变量和函数返回值必须是常量表达式。常量表达式是指在编译时可以计算出结果的表达式,其值在程序运行期间不会发生变化。常量初始化器是用于初始化constexpr变量的表达式,其值也必须在编译时确定。


3. constexpr在实际开发中的应用

a. 编译时计算示例

使用constexpr关键字进行编译时计算的一个典型示例是计算斐波那契数列:

constexpr int fibonacci(int n) 
    return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);


constexpr int fib_10 = fibonacci(10);  // 编译时计算fibonacci(10)

b. 提高代码性能

通过将一些可以在编译时确定的计算提前至编译阶段,可以减少程序的运行时开销,提高性能。例如,对于一些固定的数据,可以使用constexpr函数在编译时生成,以减少运行时的计算负担。

c. 编译时验证

使用constexpr关键字可以在编译阶段对一些值进行验证,确保代码的正确性。例如,检查编译时常量的有效性,或确保在编译时计算的值满足某些约束条件。

d. 元编程

constexpr可以与模板编程结合,实现编译时的计算和代码生成,从而提高代码的性能和灵活性。例如,可以使用constexpr函数和模板元编程实现高效的矩阵运算库。


4. constexpr与其他编译时技术的比较

a. constexpr与const

const关键字用于声明常量,表明变量的值在程序运行期间不会发生变化。constexpr关键字强调变量的值在编译时确定,因此具有更强的限制性。在适当的场景下,使用constexpr关键字可以提高代码的性能。

b. constexpr与静态变量

静态变量在程序运行期间只初始化一次,但其值在编译时不一定确定。constexpr变量和函数的值在编译时就已经确定,因此可以实现编译时计算和优化。

c. constexpr与C++14中引入的consteval

C++14中引入了consteval关键字,用于指定一个函数必须在编译时计算。constexpr关键字允许在编译时或运行时计算,具有更大的灵活性。在需要确保函数在编译时计算的场景


5. 注意事项和最佳实践

a. 合理使用constexpr

在适当的场景下使用constexpr关键字,例如当你需要在编译时计算或验证某些值时。不要过度使用constexpr,因为这可能会导致编译时间过长,从而影响开发效率。

b. 什么时候使用constexpr

constexpr关键字适用于以下场景:

  • 当你有一个值或表达式,在编译时就已知且不会在运行时改变。
  • 当你需要在编译时进行计算,以提高代码的性能或实现编译时验证。
  • 当你需要实现元编程,如编译时生成数据结构或进行编译时优化。

c. 避免编译时计算带来的复杂度

虽然constexpr可以提高代码性能,但过度使用可能导致编译时间过长,以及增加代码的复杂度。在使用constexpr时,要确保其确实有助于提高代码性能,同时不会降低代码的可读性和可维护性。

6. 总结

constexpr关键字在C++11中引入,允许开发者将变量、函数和表达式标记为在编译时确定其值的常量。
使用constexpr关键字可以提高代码的性能,减少运行时开销,并为编译时验证和元编程提供支持。
在实际开发中,合理使用constexpr关键字,结合其他编译时技术,可以实现更高效、易于维护的代码。同时,关注constexpr的使用注意事项和最佳实践,有助于编写出更加高质量、易于理解的代码。

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

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

例如本文,很多人可能不知道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还在傻傻分不清?

以上是关于解读C++ constexpr关键字的特性的主要内容,如果未能解决你的问题,请参考以下文章

C++ constexpr类型说明符

C++11新特性:16—— C++11 constexpr:验证是否为常量表达式(长篇神文)

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

constexpr 和字节序

C++ constexpr 引用多重定义

C++ 编译器优化 - 为啥需要 constexpr?