前言
C++98/03 时代,"常量"只能是字面量或简单表达式,很多数组大小、模板参数、switch case的计算必须在运行时做,导致:
-
性能损失(运行时计算)
-
无法用于模板、数组大小、static_assert 等编译期场景
-
代码里到处是 magic number 或运行时 assert
在 C++11 之前,若想在编译期计算阶乘,只能用模板特化,晦涩难懂
cpp
// C++98 模板元编程实现编译期阶乘
template <int N>
struct Factorial {
enum { value = N * Factorial<N-1>::value };
};
template <> // 特化:递归终止条件
struct Factorial<0> {
enum { value = 1 };
};
int arr[Factorial<5>::value]; // 数组大小 120,但写法像"黑话"
C++11 委员会决定引入 constexpr:让函数和变量在编译期就能计算。目标是"把能放到编译期的计算全部提前",实现零运行时开销 + 更强的类型安全。C++14 进一步放松规则,允许 if、for、局部变量,C++17 加入 if constexpr编译期分支,让模板元编程从"黑魔法"变成"正常写法"。
constexpr 的设计目标很明确:把能放到编译期的计算全部提前,实现零运行时开销 + 更强的类型安全。
constexpr 变量 + 函数(C++11 基础)
底层原理:
-
constexpr 告诉编译器:"这个东西必须在编译期算出来,否则编译错误。"
-
编译器会像执行一个 mini 解释器一样在编译阶段跑你的函数,算出结果,存到二进制里(零运行时开销)。
-
只能用编译期已知的操作(不能 new、不能 throw、不能调用非 constexpr 函数)。
C++11 中 constexpr 函数的约束非常苛刻:
-
函数体只能包含一条 return 语句(不能有其他语句)
-
不能定义局部变量
-
不能有循环
-
不能有 if,只能用三元运算符 (
?:) -
只能调用其他
constexpr函数 -
只能操作字面量类型,算术类型、引用、指针等,自定义类型需满足特定条件
代码示例:
cpp
// C++98 老写法(运行时计算)
const int size = 10 + 20; // 仅为"只读",非编译期常量
int arr[size]; //错误,size 不是编译期常量
// C++11 新写法(编译期计算)
constexpr int size = 10 + 20; // 编译期直接算出 30
int arr[size]; // 完全合法
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n-1);
}
int main() {
int arr2[factorial(5)]; // 编译期算出 120,数组大小直接确定
static_assert(factorial(5) == 120, ""); // 编译期断言,不通过则报错
}
C++11 constexpr 类与构造函数
C++11 不仅允许 constexpr 变量和函数,还引入了 constexpr 构造函数,只要其构造函数是 constexpr 且所有成员都是字面量类型,允许在编译期创建对象:
cpp
class Point {
public:
// constexpr 构造函数:必须在初始化列表中完成所有成员初始化
constexpr Point(int x, int y) noexcept : x_(x), y_(y) {}
// constexpr 成员函数:必须是 const 的(C++11 限制,C++14 可放松)
constexpr int getX() const noexcept { return x_; }
constexpr int getY() const noexcept { return y_; }
private:
int x_, y_;
};
// 编译期创建对象
constexpr Point origin(0, 0);
constexpr Point p(10, 20);
// 直接用于数组大小、模板参数等编译期场景
int arr[p.getX()]; // 合法:数组大小为 10
static_assert(origin.getY() == 0); // 编译期断言通过
-
constexpr构造函数的函数体在 C++11 中必须为空,所有初始化必须在初始化列表完成。 -
constexpr对象的成员变量必须在编译期初始化。
C++14 constexpr 的放松
C++11 里 constexpr 函数只能有一条 return 语句,不能有 if、for、局部变量。C++14 对 constexpr 函数的限制做了大幅松绑,使其几乎可以像普通函数一样编写:
-
可以写 if、for、while、switch
-
可以有局部变量,但必须是字面量类型,不能是
std::string等非字面量类型。 -
可以调用其他 constexpr 函数
-
可以有多条 return 语句
-
允许
constexpr成员函数
| 限制项 | C++11 | C++14 |
|---|---|---|
| 函数体语句 | 只能有一条 return 语句 |
允许 if、for、while、局部变量等 |
| 局部变量 | 不允许 | 允许,但必须初始化 |
| 修改成员变量的函数 | 必须是 const |
允许非 const(可修改对象状态) |
对上述代码进行改造:
cpp
// C++11 老写法(只能一条 return)
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n-1);
}
// C++14 新写法(可以写循环、局部变量)
constexpr int sum_up_to(int n) {
int sum = 0;
for (int i = 1; i <= n; ++i) {
sum += i;
}
return sum;
}
constexpr int s = sum_up_to(100); // 编译期算出 5050
除此之外,再提供一个编译期计算最大公约数的例子:
cpp
constexpr int gcd(int a, int b) noexcept {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
static_assert(gcd(48, 18) == 6); // 编译期验证通过
注意,constexpr 函数里不能调用非 constexpr 函数,不能使用 new/delete、throw、goto 等。
C++17 if constexpr
设计原因: 以前模板里要根据类型做不同行为,只能用 SFINAE或 std::enable_if,代码又臭又长。
底层原理: if constexpr 的不满足分支在编译期被直接丢弃,不会生成代码,不会实例化模板,彻底解决 "死代码" 问题。
代码示例:
cpp
#include <iostream>
#include <type_traits> // 类型特征头文件,后面会详细讲
template<typename T>
void print(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "整数: " << value << '\n';
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "浮点: " << value << '\n';
} else {
std::cout << "其他类型: " << value << '\n';
}
}
int main() {
print(42); // 输出"整数: 42"
print(3.14); // 输出"浮点: 3.14"
print("hello"); // 输出"其他类型: hello"
}
if constexpr 的优势,不满足分支不实例化
普通 if 的所有分支都会被编译,即使条件不满足,而 if constexpr 的不满足分支会被直接丢弃,因此即使分支里的代码对当前类型不合法也不会报错:
cpp
template <typename T>
auto get_value(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // 如果 T 不是指针,这行不会被实例化,所以不会报错!
} else {
return t;
}
}
int main() {
int x = 42;
get_value(&x); // 返回 42(T 是 int*,走第一个分支)
get_value(x); // 返回 42(T 是 int,走第二个分支,不会因 *t 报错)
}
常见坑
-
constexpr 函数里调用了非 constexpr 函数 → 编译错误
-
constexpr 变量没初始化或初始化用了运行时值 → 错误
-
if constexpr 里放运行时表达式 → 错误,必须是编译期常量表达式
-
constexpr函数递归深度限制→ 编译器默认限制 512-1024 层,太深会编译失败 -
误以为
const int x = func();是constexpr→ 必须显式写constexpr,否则仍是运行时计算
建议:
-
所有 "可以编译期算" 的函数都加上
constexpr -
数组大小、模板参数、
static_assert首选constexpr -
C++17 后大量用
if constexpr替换 SFINAE -
永远给
constexpr函数加noexcept,因为constexpr函数通常是纯函数,noexcept告诉编译器它不会抛异常,有助于优化。
QA:
- constexpr 和 const 区别?
const只是 "只读",可以在运行时赋值;constexpr必须编译期算出值,可以用在数组大小、模板参数等地方。
- if constexpr 为什么比普通 if 好?
不满足的分支在编译期被完全丢弃,不会生成代码,不会实例化模板,编译更快、最终二进制更小。
- constexpr 函数能有多复杂?
C++14 后几乎能写普通函数能写的一切(循环、局部变量),C++20 甚至能用
std::vector等(但 11/14/17 还是有一定限制)。
- 什么时候必须用 constexpr?
模板参数、数组大小、
static_assert、编译期常量表。
- constexpr 函数可以在运行时调用吗?
可以。如果传入的参数是运行时值,
constexpr函数会退化为普通函数在运行时执行;只有当传入编译期常量且上下文要求编译期结果时,才会在编译期计算:
cppconstexpr int add(int a, int b) { return a + b; } int main() { int x = 1, y = 2; int z = add(x, y); // 运行时计算(x、y 是运行时值) constexpr int w = add(3, 4); // 编译期计算(3、4 是常量) }