C++ 的核心设计哲学之一是 "零开销抽象",而编译期计算正是实现这一哲学的关键手段。传统 C++ 中,计算只能在运行时进行,很多可以提前确定的结果无法被编译器优化,导致不必要的运行时开销。从 C++11 开始引入的constexpr关键字,彻底改变了这一现状,它允许将计算从运行时转移到编译期,实现真正的 "零开销"。
1:constexpr和常量表达式
1:核心内容
常量表达式 是指值不会改变并且在编译过程中就能得到计算结果的表达式。字面值、用常量表达式初始化的const对象都是常量表达式;但用变量初始化的const对象不是常量表达式。
constexpr(constant expression)是 C++11 引入的关键字,用于指定常量表达式:
- 修饰变量:
constexpr变量一定是常量表达式,且必须用常量表达式初始化 - 修饰指针:
constexpr修饰的指针是顶层 const,即指针本身不可修改
cpp
int size()
{
int n = 10;
return n;
}
int main()
{
const int a = 1; // a是常量表达式
const int b = a + 1; // b是常量表达式
int c = 1; // c不是常量表达式
const int d = c; // d不是常量表达式
const int e = size(); // e不是常量表达式
// 常量表达式可以做数组大小(VS不支持变长数组)
int arr[a];
constexpr int aa = 1;
constexpr int bb = aa + 1;
// constexpr int cc = c; // 报错:c不是常量表达式
// constexpr int cc = size(); // 报错:size()不是常量表达式
// constexpr修饰指针是顶层const
// constexpr int* p1 = &d; // 报错:权限放大
const int* p2 = &d;
constexpr const int* p3 = &d; // constexpr修饰p3本身,const修饰*p3
return 0;
}
2:const和constexpr的区别
| 特性 | const | constexpr |
|---|---|---|
| 初始化时机 | 编译时或运行时 | 必须在编译时 |
| 语义 | 只读变量(运行时不可修改) | 编译期常量(编译时就确定值) |
| 用途 | 保护变量不被修改 | 数组大小、模板参数、编译期计算 |
| 修饰指针 | 可以是顶层或底层 const | 只能是顶层 const |
示例:
cpp
// const可以运行时初始化
int x = 10;
const int y = x; // 正确:运行时初始化
// constexpr必须编译时初始化
// constexpr int z = x; // 错误:x是运行时变量
2:constexpr函数
1:核心内容
C++11 允许将函数声明为constexpr,使其可以在编译期被调用并计算结果。C++11 对constexpr函数有严格限制:
- 参数和返回值必须是字面值类型(整形、浮点型、指针、引用等)
- 返回值类型不能是
void - 函数体只能包含一条 return 语句
- 不能定义局部变量、循环、条件判断等控制流
- 返回值必须是常量表达式
cpp
#include<iostream>
using namespace std;
constexpr int size()
{
return 10;
}
constexpr int func(int x)
{
return 10 + x;
}
constexpr int factorial(int n)
{
return n <= 1 ? 1 : n * factorial(n - 1);
}
// 错误:包含局部变量和IO操作
constexpr int fxx(int x)
{
int i = x;
i++;
cout << i << endl;
return 10 + x;
}
int main()
{
// 编译时N1被直接替换为10,constexpr函数默认是inline
constexpr int N1 = size();
int arr1[N1];
// 传常量表达式时,func在编译期计算
constexpr int N2 = func(10);
int arr2[N2];
// 传运行时变量时,func在运行时计算
int i = 10;
// constexpr int N3 = func(i); // 报错:i是运行时变量
int N4 = func(i); // 不报错:运行时调用
constexpr int fact5 = factorial(5); // 编译时计算出120
// constexpr int N5 = fxx(10); // 报错:fxx不符合constexpr要求
return 0;
}
2:constexpr构造函数与成员函数
constexpr不能直接修饰自定义类型,但可以修饰类的构造函数,使该类的对象可以成为编译期常量:
- 所有成员变量必须是字面值类型
- 必须在初始化列表中初始化所有成员变量
- 构造函数体必须为空
- 析构函数必须是平凡的(不做任何实际清理工作)
constexpr成员函数自动成为const成员函数,不能修改对象的成员变量,且不能是虚函数。
cpp
#include<iostream>
using namespace std;
class Date
{
public:
constexpr Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{
// cout << "构造函数" << endl; // 错误:不能有IO操作
}
constexpr int GetYear() const
{
return _year;
}
private:
int _year;
int _month;
int _day;
};
template<typename T>
constexpr T Func(T t)
{
return t;
}
int main()
{
int x = 2025;
// constexpr Date d0(x, 9, 8); // 报错:x是运行时变量
constexpr Date d1(2025, 9, 8); // 编译期构造对象
constexpr int y = d1.GetYear(); // 编译期调用成员函数
Date d2(2025, 8, 11); // 运行时构造对象
int z = d2.GetYear(); // 运行时调用
string ret1 = Func("111111"); // 普通函数(constexpr被忽略)
constexpr int ret2 = Func(10); // 编译期调用
return 0;
}
3:C++11constexpr的限制原因
C++11 对constexpr函数的严格限制主要是为了降低编译器的实现难度。当时编译器还无法处理复杂的编译期控制流,因此只能支持最简单的单 return 语句和递归。这些限制在后续 C++ 版本中被逐步放宽。
3:constexpr在C++14中的演进
1:核心内容
C++14 最显著的改进是大幅放宽了对 constexpr 函数的限制,使其语法和功能更接近普通函数:
- 允许声明和初始化局部变量(只要在 constexpr 上下文中使用)
- 支持
if条件分支、for/while循环、switch语句等控制流 - 允许多条
return语句 - 支持更复杂的返回类型(自定义类、
std::array等)
cpp
// C++14允许的constexpr函数示例
constexpr int factorial(int n) {
int res = 1; // 允许局部变量
for (int i = 2; i <= n; ++i) { // 允许循环
res *= i;
}
return res; // 单一return
}
constexpr size_t stringLength(const char* str) {
size_t len = 0;
while (str[len] != '\0')
++len;
return len;
}
constexpr size_t len = stringLength("Hello"); // 编译期计算:5
2:支持复杂的返回类型
C++14 允许constexpr函数返回非基本类型,包括自定义类、std::array等符合 constexpr 要求的复合类型:
cpp
#include<iostream>
#include<vector>
#include<array>
using namespace std;
struct Point {
constexpr Point(double x, double y): x(x), y(y) {}
double x, y;
};
constexpr Point midpoint(Point a, Point b) {
return Point((a.x + b.x) / 2, (a.y + b.y) / 2);
}
constexpr std::array<int, 5> createArray() {
std::array<int, 5> arr{};
for (size_t i = 0; i < arr.size(); ++i) {
arr[i] = i * i;
}
return arr;
}
constexpr int fibonacci(int n) {
return (n <= 1) ? n : (fibonacci(n - 1) + fibonacci(n - 2));
}
int main()
{
Point p1 = midpoint({1.1, 1.1}, {2.2, 2.2}); // 运行时
constexpr Point p2 = midpoint({1.1, 1.1}, {2.2, 2.2}); // 编译期
constexpr std::array<int, 5> a1 = createArray(); // 编译期生成数组
constexpr int fibArray[] = {
fibonacci(0), fibonacci(1), fibonacci(2), fibonacci(3),
fibonacci(4), fibonacci(5), fibonacci(6), fibonacci(7)
};
return 0;
}
3:C++14的constexpr的实际应用
C++14 的constexpr已经可以用于很多实际场景,比如:
- 编译期字符串哈希
- 编译期数学计算(如三角函数、矩阵运算)
- 编译期数据结构(如静态数组、链表)
4:constexpr在C++17的演进
1:核心内容
if constexpr是 C++17 引入的革命性特性,它允许在编译时根据常量表达式的结果决定编译哪部分代码,未选择的分支代码不会被编译成指令,直接被丢弃。
cpp
template <typename T>
auto get_value(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // 仅当T为指针类型时实例化
} else {
return t; // 非指针类型时实例化
}
}
// 使用示例
int x = 42;
auto v1 = get_value(x); // 返回x本身
auto v2 = get_value(&x); // 解引用返回42
2:if constexpr和普通if
| 特性 | if constexpr | 普通 if |
|---|---|---|
| 执行时机 | 编译时 | 运行时 |
| 未选择分支 | 不编译,直接丢弃 | 编译但不执行 |
| 表达式要求 | 必须是编译时常量表达式 | 任何表达式 |
| 用途 | 模板分支、类型分发 | 运行时逻辑分支 |
关键区别 :普通 if 的两个分支都会被编译,即使其中一个永远不会执行;而if constexpr的未选择分支不会被编译,因此可以包含在某些类型下无效的代码。
3:constexpr lambda表达式
C++17 允许将 lambda 表达式标记为constexpr,使其可以在编译期被调用:
- 捕获必须是编译期常量
- 函数体需满足 constexpr 函数的要求
cpp
int main()
{
// constexpr lambda示例
constexpr int n = 10;
int y = 0;
constexpr auto square = [n](int x) constexpr { return x * x * n; };
constexpr int result = square(5); // 编译期计算:250
return 0;
}
5:constexpr在C++20的演进(以下代码因为标准太新,可能导致编译不通过)
1:核心内容:动态内存分配的编译期支持
C++20 允许在constexpr上下文中使用new/delete进行动态内存分配,这使得std::vector和std::string等容器的编译期实现成为可能。但有一个关键限制:所有分配的内存在编译期必须被释放。
cpp
constexpr int dynamic_memory_example() {
int* p = new int{42}; // 编译期分配
int value = *p;
delete p; // 必须显式释放
return value;
}
int main()
{
constexpr int v = dynamic_memory_example(); // 42
return 0;
}
2:标准库逐步constexpr化
C++20 开始对标准库进行大规模的 constexpr 化,很多常用算法和容器现在可以在编译期使用:
<algorithm>中的std::find、std::sort等<array>的全部操作<vector>的部分操作(受内存释放限制)
cpp
#include<iostream>
#include<vector>
#include<array>
#include<string>
#include<algorithm>
using namespace std;
// 编译报错:vector的析构函数在编译期无法释放内存
// constexpr std::vector<int> create_vector() {
// std::vector<int> v{1, 2, 3};
// v.push_back(4);
// return v;
// }
constexpr auto sort_example() {
std::array<int, 5> arr{5, 3, 4, 1, 2};
std::sort(arr.begin(), arr.end()); // 编译期排序
return arr;
}
int main()
{
// constexpr auto vec = create_vector(); // 编译失败
constexpr auto sorted = sort_example(); // {1,2,3,4,5}
constexpr auto it2 = find(sorted.begin(), sorted.end(), 4);
static_assert(*it2 == 4, "编译期查找");
return 0;
}
3:try-catch的全面支持
C++20 允许在constexpr函数中使用try-catch块,但有一个限制:不能真正抛出异常(否则不是常量表达式)。主要用于模板约束和编译期错误检测。
cpp
#include<iostream>
using namespace std;
constexpr int safe_divide(int a, int b) {
try {
if (b == 0)
throw "Division by zero";
else
return a / b;
}
catch (...) {
return 0; // 编译期异常处理
}
}
int main()
{
constexpr int val1 = safe_divide(10, 2); // 5
// constexpr int val2 = safe_divide(10, 0); // 报错:抛出异常不是常量表达式
return 0;
}
4:C++20 constexpr的其他增强
- constexpr 联合体:可以在编译期改变联合体的活跃成员
- constexpr mutable 成员 :
mutable修饰的成员变量可以在constexpr成员函数中修改 - constexpr 虚函数:支持编译期多态调用
cpp
// constexpr虚函数示例
class Base {
public:
virtual constexpr int value() const { return 1; }
};
class Derived : public Base {
public:
constexpr int value() const override { return 2; }
};
constexpr int get_value(const Base& b) {
return b.value(); // 编译期多态调用
}
int main() {
constexpr int ret1 = get_value(Base()); // 1
constexpr int ret2 = get_value(Derived()); // 2
return 0;
}
6:C++20的consteval
1:核心内容
constexpr的核心思想是 "允许在编译期进行计算 ",但它也可以在运行时计算,具体取决于调用上下文。这种不确定性在某些场景下会导致问题。
consteval是为了解决这个问题而引入的,它的核心思想是 "必须 在编译期求值 ",被称为立即函数 。如果一个consteval函数不能在编译时被求值,程序将无法通过编译。
cpp
constexpr int square(int x) {
return x * x;
}
int main() {
// 场景1:编译时求值
constexpr int const_val = square(10); // 必须在编译时计算
int array[const_val];
// 场景2:运行时求值
int runtime_input = 5;
int runtime_val = square(runtime_input); // 运行时调用
return 0;
}
// 将square改为consteval修饰
consteval int square_consteval(int x) {
return x * x;
}
// int main() {
// constexpr int const_val = square_consteval(10); // 正确
// int runtime_input = 5;
// int runtime_val = square_consteval(runtime_input); // 报错:必须编译期求值
// }
2:constexpr vs consteval 使用场景对比
| 场景 | 推荐使用 |
|---|---|
| 函数既需要编译期调用也需要运行期调用 | constexpr |
| 函数只能在编译期调用(如编译期计算、生成常量) | consteval |
| 函数有副作用(如 IO 操作) | 都不使用 |
7:C++20的constinit
1:核心内容
constexpr和constinit都可以修饰变量,它们的核心区别在于初始化时机 和可变性:
constexpr:必须在编译时初始化,且值在整个程序生命周期内不可变(是常量)constinit:必须在编译时初始化,但其值在运行时可以改变(非常量)
constinit只能用于具有静态存储期 或线程存储期的变量(如全局变量、static 变量、thread_local 变量),不能用于函数内的局部变量。
cpp
#include<iostream>
consteval int square(int n) {
return n * n;
}
constexpr int compute_value() {
return 42;
}
constinit int global = compute_value(); // 正确
constinit int squared_value = square(5); // 正确
// 确保复杂对象在编译期初始化
class ComplexInit {
int value;
public:
constexpr ComplexInit(int v) : value(v) {}
};
constinit ComplexInit obj{42}; // 全局对象确保编译期初始化
int main() {
// constinit int local = 10; // 错误:只能用于静态存储期变量
squared_value = 30; // 可以修改
return 0;
}
2:解决全局变量初始化顺序问题
C++ 中,不同编译单元(.cpp 文件)中的全局变量、静态变量的初始化顺序是未定义的,这可能导致 "静态初始化顺序灾难"。constinit可以完美解决这个问题,因为它保证变量在编译期就已经初始化完成。
cpp
// 问题代码:a和b的初始化顺序不确定
// a.cpp
int a = 42;
// b.cpp
extern int a;
int b = a; // 不安全:如果b先初始化,a的值是未定义的
// 解决方案:使用constinit
// a.cpp
constinit int a = 42;
// b.cpp
extern constinit int a;
constinit int b = a; // 安全:a保证已在编译期初始化
3:constexpr、consteval、constinit 三者对比
| 特性 | constexpr | consteval | constinit |
|---|---|---|---|
| 修饰对象 | 变量、函数 | 函数 | 变量 |
| 初始化时机 | 编译时 | 编译时(函数调用) | 编译时 |
| 可变性 | 不可变 | 不适用 | 可变 |
| 适用变量存储期 | 所有 | 不适用 | 静态 / 线程存储期 |
| 核心语义 | 允许编译期计算 | 强制编译期计算 | 强制编译期初始化 |
8:常见的误区和坑
1:constexpr函数不能有副作用
cpp
constexpr int func(int x) {
cout << x << endl; // 错误:IO操作有副作用
return x + 1;
}
2:consteval不能用运行时参数调用
cpp
consteval int square(int x) { return x * x; }
int x = 10;
// int y = square(x); // 错误:x是运行时变量
3:constinit只能用于静态存储期的变量
cpp
int main() {
// constinit int x = 10; // 错误:局部变量是自动存储期
static constinit int y = 10; // 正确:static变量是静态存储期
}
4:C++20的constexpr动态分配必须编译器释放
cpp
constexpr int func() {
int* p = new int{42};
return *p; // 错误:内存泄漏,编译期必须释放
}
5:if constexpr的条件必须是编译常量
cpp
int x = 10;
// if constexpr (x > 5) {} // 错误:x是运行时变量
9:总结
- 优先使用 constexpr 修饰编译期常量 :凡是在编译期就能确定值的变量,都应该用
constexpr修饰,而不是const。 - 将纯函数声明为 constexpr :如果一个函数没有副作用,且输入确定时输出也确定,应该将其声明为
constexpr,让编译器决定是否在编译期计算。 - 使用 consteval 强制编译期计算 :对于只能在编译期使用的函数(如生成编译期常量、计算哈希值),使用
consteval修饰,避免意外的运行时调用。 - 使用 constinit 解决全局变量初始化顺序问题 :对于需要跨编译单元访问的全局变量,使用
constinit修饰,确保其在编译期初始化完成。 - 不要过度使用编译期计算:复杂的编译期计算会显著增加编译时间,只有在能带来明显运行时性能提升的场景下才使用。
- 优先使用 if constexpr 替代 SFINAE :C++17 及以上版本,使用
if constexpr进行模板分支,代码更清晰易读。