19、constexpr与consteval:编译期求值,constexpr函数与constexpr构造函数
好,咱们今天聊一个让C++真正“硬核”起来的话题——编译期求值。
说白了,就是让编译器在编译阶段就把活儿干完,而不是等到程序跑起来才吭哧吭哧算。我刚开始接触模板元编程那会儿,总觉得这东西玄乎,后来用多了才发现,编译期求值才是C++性能优化的“杀手锏”。
19.1 编译期求值的本质
先问个问题:为什么我们需要编译期求值?
你想想看,有些计算是固定的,比如数组大小、数学常量、配置参数。这些值一旦确定就不会变。如果让它们在运行时计算,每次启动程序都要重复算一遍,这不是浪费吗?
编译期求值的核心思想就是:把能提前算的都提前算好。这样程序跑起来就更快,内存占用也更少。
关键点:编译期求值不是C++的“新功能”,而是C++11之后逐步完善的“老功能”。constexpr从C++11开始引入,到C++14放宽限制,再到C++17、C++20不断进化。
19.2 constexpr:编译期常量的基石
constexpr是C++11引入的关键字,用来声明“可以在编译期求值的表达式”。
我个人的习惯是:只要一个值在编译期就能确定,就尽量用constexpr。这不仅仅是性能优化,更是一种设计态度——告诉读者和编译器:“这个值是固定的,别担心它会在运行时变化。”
19.2.1 constexpr变量
最简单的用法就是声明编译期常量:
constexpr int arraySize = 100;
constexpr double pi = 3.141592653589793;
constexpr int factorialResult = 120; // 5! = 120
这些变量在编译期就确定了值,可以用在需要编译期常量的地方,比如数组大小:
int myArray[arraySize]; // 没问题,arraySize是编译期常量
小技巧:constexpr变量默认是const的,但const变量不一定是constexpr。const只保证“不修改”,constexpr保证“编译期可知”。
19.2.2 constexpr函数
constexpr函数才是真正的重头戏。它允许你写一个函数,既能用在编译期,也能用在运行期。
我记得刚学constexpr函数时,有个误区:以为constexpr函数必须“纯编译期”。其实不是——constexpr函数是“双模”的:如果参数是编译期常量,就在编译期求值;如果参数是运行时变量,就退化为普通函数。
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// 编译期求值
constexpr int result1 = factorial(5); // 编译期就算出120
// 运行时求值
int x = 5;
int result2 = factorial(x); // 运行时才计算
是不是很灵活?同一个函数,两种用法。
不过要注意,C++11对constexpr函数限制很严格——函数体只能有一条return语句。C++14放宽了这个限制,允许使用局部变量、循环、if语句等。我个人建议:如果项目支持C++14以上,放心用更灵活的写法。
// C++14及以后,可以这样写
constexpr int factorial_cpp14(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}
19.3 constexpr构造函数:编译期创建对象
constexpr不仅能用于普通函数,还能用于构造函数。这意味着你可以在编译期创建对象。
我在项目中遇到过这样一个场景:需要定义一组固定的颜色值,用于UI渲染。如果每次启动都重新计算,不仅慢,而且没必要。用constexpr构造函数就完美解决了。
class Color {
public:
constexpr Color(unsigned char r, unsigned char g, unsigned char b)
: red_(r), green_(g), blue_(b) {}
constexpr unsigned char red() const { return red_; }
constexpr unsigned char green() const { return green_; }
constexpr unsigned char blue() const { return blue_; }
private:
unsigned char red_;
unsigned char green_;
unsigned char blue_;
};
// 编译期创建对象
constexpr Color white(255, 255, 255);
constexpr Color black(0, 0, 0);
constexpr Color red(255, 0, 0);
你看,这些颜色对象在编译期就创建好了,运行时直接使用,没有任何构造开销。
注意:constexpr构造函数必须满足:所有成员变量都用constexpr方式初始化,函数体为空(C++11)或只包含constexpr允许的语句(C++14+)。另外,析构函数不能是constexpr(C++20之前)。
19.4 consteval:强制编译期求值
constexpr函数有个“小毛病”:它不强制在编译期求值。如果参数是运行时变量,它就变成普通函数了。有时候,我们就是想让某个函数必须在编译期执行,怎么办?
C++20引入了consteval关键字。它和constexpr很像,但有一个关键区别:consteval函数只能在编译期求值。如果试图在运行时调用它,编译器会报错。
consteval int square(int x) {
return x * x;
}
constexpr int result1 = square(5); // 没问题,编译期求值
int y = 5;
// int result2 = square(y); // 编译错误!y不是编译期常量
为什么要用consteval?我举个例子:假设你写了一个函数,用来生成编译期哈希值。这个函数如果被误用在运行时,不仅性能差,还可能产生错误的结果。用consteval就能从根源上杜绝这种误用。
核心区别:
- constexpr:可以在编译期求值,也可以在运行期求值(“双模”)
- consteval:只能在编译期求值(“单模”)
19.5 编译期求值的实际应用
说了这么多理论,咱们看看实际项目中怎么用。
19.5.1 编译期查表
我曾经写过一个信号处理库,需要频繁计算正弦值。每次调用sin()太慢了,于是我用constexpr生成了一个查找表:
template<size_t N>
class SinTable {
public:
constexpr SinTable() : values_{} {
for (size_t i = 0; i < N; ++i) {
values_[i] = std::sin(2.0 * pi * i / N);
}
}
constexpr double operator[](size_t index) const {
return values_[index];
}
private:
double values_[N];
};
constexpr SinTable<1024> sinTable; // 编译期生成1024个正弦值
这样,运行时查表比调用sin()快了将近10倍。而且因为是在编译期生成的,没有任何运行时初始化开销。
19.5.2 编译期类型计算
constexpr还能和模板结合,实现编译期类型计算。比如判断一个类型是否是整数:
template<typename T>
constexpr bool is_integral_v = false;
template<>
constexpr bool is_integral_v<int> = true;
template<>
constexpr bool is_integral_v<long> = true;
// 使用
static_assert(is_integral_v<int>, "int should be integral");
static_assert(!is_integral_v<double>, "double should not be integral");
当然,C++标准库已经提供了std::is_integral_v,这里只是演示原理。
19.6 避坑指南
讲几个我踩过的坑,希望对你有帮助。
坑一:constexpr函数不能调用非constexpr函数
这个限制在C++11/14/17都很严格。C++20放宽了一些,但最好还是记住:constexpr函数体内只能调用其他constexpr函数。
constexpr int badFunction(int x) {
// return std::sin(x); // 错误!std::sin不是constexpr(C++20之前)
return x * x; // 正确
}
坑二:constexpr构造函数不能有虚函数
这个很好理解——虚函数需要运行时多态,和编译期求值矛盾。
坑三:constexpr函数在调试时可能被优化掉
我曾经在调试时发现,constexpr函数在Release模式下确实在编译期求值了,但在Debug模式下却变成了运行时调用。这是因为编译器在Debug模式下默认不进行编译期求值优化。解决方案是:用static_assert验证编译期求值是否生效。
constexpr int add(int a, int b) { return a + b; }
// 验证编译期求值
static_assert(add(3, 4) == 7, "compile-time evaluation failed");
19.7 知识体系图
下面这张图总结了本章的核心知识结构:
19.8 总结
constexpr和consteval是C++编译期求值的两大法宝。constexpr提供了“双模”灵活性,consteval提供了“强制编译期”的安全性。
我个人建议:
- 能用constexpr的地方,尽量用——它不会让代码变慢,只会让代码更快
- 需要强制编译期求值时,用consteval——C++20之后的好东西,别浪费
- 用static_assert验证编译期求值——确保你的constexpr真的在编译期执行了
嗯,编译期求值这块内容就讲到这里。记住一句话:编译期多算一点,运行期就快一点。这是C++性能优化的黄金法则。
公众号:蓝海资料掘金营,微信deep3321