Modern C++ 之 constexpr : 真正的 "常量"
constexpr
关键字在 C++11 中被引入标准, 它用于定义一个常量. 这里常量的含义是编译器即可确定的结果, 这个结果可以是 :
- 变量的值
- 函数发返回值
- 条件分支的判断结果 (C++17 起)
constexpr
与传统 C++ 中名字相似的 const
相比, 二者并不是子集与超集的关系, 但在语义上确有一些交集.
如何定义一个常量?
在传统 C++ 中, 主要分为两个流派: 宏定义 或 常量 const
.
1 |
|
而在现代 C++ 中, 更推荐使用 constexpr
定义常量, 以替代宏定义与常量 const
.
1 | constexpr int Constexpr_Number = 128; |
使用 constexpr
避免宏定义的污染性
预处理语句无视代码块的访问限制, 这将污染代码, 并可能在难以预见的地方留下隐患. 现假设如下情况 : 两个进行联合编译的工程内均使用了一个常量 : 当前平台渲染器的最大数量. 代码如下:
1 | // renderer project |
即便这个同名常量在两个语境下可能表达的具体含义是不一样的, brabbit::media 中的宏依然会污染到主要工程. 这种情况下使用 const
定义常量可能会与宏的命名冲突, 当然这还只是小问题, 改一个名字就好了. 但如果我们定义了同名的宏, 则可能会在运行期影响 brabbit::media 的配置.
又如微软的经典历史遗留问题 :
1 | // stdlib.h : |
使用 constexpr
定义一个常量不会造成代码的污染 :
1 | // renderer project |
使用 constexpr
避免 const
的双重含义 : “常量” 与 “只读变量”
定义常量的目的 : 在编译期就确定值, 从而达成运行期零开销
1 | // 三者均为运行期零开销的常量 |
但使用 const
关键字时, 需要判断其是常量还是只读量:
1 | // lib.h |
C 中的 const 是只读变量,有自己的储存空间,能通过地址间接修改其的值;
C++ 中的 const 是常量或只读变量, 存放在符号表中. 当对其进行引用时才会临时分配栈空间.
使用 constexpr
可以区分常量与只读变量 :
1 | int number = 128; // ok : 变量, 在运行期确定值 |
上述这些案例, 只要你足够细心, 理论上也是可以使用 const
完全替代 constexpr
的, 但下面这些情况就是只能使用 constexpr
而非 const
了.
使用 constexpr
修饰函数与分支
constexpr
关键字除了可以对变量进行修饰外, 还可以对函数与分支语句进行修饰.
使用 constexpr
进行修饰的函数, 其返回值必须是编译期就可以计算出来的. 这意味着所有参与计算出返回值的变量都必须是编译期就可以确定其值的. 就如同通过一个对象的 const
引用只能访问 const
成员函数一样.
1 | int constexpr count_array_max_size() { |
constexpr if
常量分支在 C++17 时被引入标准库. 使用 constexpr
修饰分支语句时, 判断条件也必须是编译期就可以确定的. 如同修饰函数一样, 所有参数者都必须是一个常量.
1 | bool constexpr check_condition() { return false; } |
引申 : 利用编译器对 constexpr
的优化策略提高代码可读性
编译器对判断一段代码是否可被安全的优化这件事, 很大程度上取决于这段代码所使用的修饰符. 如被 constexpr
或 const
修饰过的变量, 编译器会尽可能地在编译期内就确定变量的值; 又如被 inline
修饰过的函数, 编译器将更倾向于在调用该函数的地方直接展开函数而非进行函数调用.
但是不同的关键字对代码优化起到的影响效果也是不一样的, 对于 inline
编译器大多数情况下只是将其视作”建议”, 对于 const
编译器则需要严格地把编译期间所有可计算值的变量加入符号表内. 反观 constexpr
, 这是一个约束性比 const
更强的关键字, 因此编译器甚至会将 constexpr
修饰过的函数返回值都放入符号表内.
在遇到某些复杂但可预测的数学计算时, 利用这一特性可以极大提高代码可读性. 以下是约翰卡马克在 <<雷神之锤3>> 中写的的快速求平方根倒数算法 :
1 | // /code/game/q_math.c |
这个函数的效率是标准库版本的 4 倍. 而最令人费解的地方就在于那个神秘数字 0x5f3759df, 为了解释这个数字的来由, 一些学者甚至为此发表过论文. 从现代 C++ 的眼光来看, 若能够利用上编译器对 constexpr
函数优化的特性, 则可以在不增加运行期开销的情况下极大增强代码可读性 :
1 | int constexpr count_magic_number() { |
对于神秘数字 0x5f3759df, 相关论文对其的解释是这个函数利用了 内存位运算时的工作原理 与 牛顿迭代法的特性, 把精度控制在常用浮点数可表示的范围内. 想了解具体原理可以点击下面的链接:
https://en.wikipedia.org/wiki/Fast_inverse_square_root
https://wenku.baidu.com/view/80b84d1fb7360b4c2e3f644b.html
https://zhuanlan.zhihu.com/p/74728007
https://blog.csdn.net/xtlisk/article/details/51249371
constexpr
的演化
constexpr
一开始只是为了修复 const
拥有双重含义这个历史遗留问题. 但随着后续的逐步发展, 它已成为现代 C++ 的重要关键字. 以下是 constexpr
的发展历程简述 :
- C++11 引入
constexpr
关键字, 允许修饰变量与函数; - C++14 起使用
constexpr
修饰类成员函数的前提是该成员函数已经被const
修饰过; - C++14 起允许使用
constexpr
修饰std::array
,std::tuple
,std::chrono
等类型; - C++17 起允许使用
constexpr
修饰分支语句以及 lambda 表达式; - C++20 起允许在进行
constexpr
评估时使用临时的std::vector
,std::string
等类型变量;
虽然
std::vector
,std::string
等类型可以被constexpr
利用, 但不代表它们的构造函数可以在编译期间执行. 这本质上是让这些组件在设计上支持常量这个特性 (主要是通过对std::allocator
进行特殊设计), 而非constexpr
本身功能扩展了.