1. constexpr 演进
constexpr 从 C++11 的"勉强可用"到 C++23 的"几乎完备",经历了漫长的演进过程。理解这个演进历史,有助于理解各个标准的能力边界。
1.1 各标准里程碑
| 标准 | 关键特性 | 限制 |
|---|---|---|
| C++11 | constexpr 函数、constexpr 变量 | 只能有一个 return 语句,不能有局部变量 |
| C++14 | 放宽 constexpr 限制 | 允许局部变量、多语句 |
| C++17 | constexpr if、static_assert | 仍不能分配堆内存 |
| C++20 | constexpr new/delete、constexpr std::array | 虚拟函数、动态多态仍不支持 |
| C++23 | constexpr std::vector、constexpr try/catch | 大部分 STL 容器可用 |
1.2 constexpr if 的革命性意义
C++17 的 constexpr if 解决了 SFINAE 的复杂性:
SFINAE 方式 (C++14)
// 使用 SFINAE 选择重载
template<typename T,
typename = std::enable_if_t<
std::is_integral<T>::value>>
T square(T x) { return x * x; }
template<typename T,
typename = std::enable_if_t<
std::is_floating_point<T>::value>>
T square(T x) { return x * x; }
constexpr if 方式 (C++17+)
template<typename T>
T square(T x) {
if constexpr (std::is_integral_v<T>) {
return x * x;
} else if constexpr (std::is_floating_point_v<T>) {
return x * x;
} else {
static_assert(false, "unsupported type");
}
}
✅ constexpr if 的优势
- 编译期只实例化满足条件的分支,错误消息更清晰
- 无需
enable_if的模板参数技巧 - 可以直接访问模板参数的类型成员
2. 编译期求值边界
2.1 什么是常量求值上下文
编译器在以下上下文中进行常量求值(constant evaluation):
- ✓ constexpr 变量初始化
- ✓ constinit 变量初始化
- ✓ 模板参数求值
- ✓ static_assert 条件
- ✓ if constexpr 条件
- ✓ constexpr 函数调用(满足条件时)
2.2 编译期 vs 运行期边界
关键问题:编译器如何决定在编译期还是运行期执行?
// 编译器根据上下文决定是否编译期求值
// 1. 模板参数 - 必须在编译期求值
template<auto N = compute_value()> // 编译期调用
struct Holder { static constexpr auto value = N; };
// 2. constexpr 变量 - 可能在编译期求值
constexpr auto x = compute_value(); // 编译期(如果可能)
// 3. 普通变量 - 绝不在编译期求值
auto y = compute_value(); // 总是运行期
// 4. 运行时才能确定的值,无法编译期计算
std::cin >> x; // 运行期输入,无法编译期
auto z = x * 2; // 取决于 x 的求值时间
2.3 触发编译期计算的技巧
// 强制编译期计算的常见模式
// 模式1: 使用模板参数接收结果
template<auto Value>
struct 强迫编译期求值 {
static constexpr auto value = Value;
};
// 模式2: 使用 constinit
constinit auto forced_compile_time = compute_value();
// 模式3: 在 static_assert 中使用
static_assert(compute_value() > 0);
// 模式4: 使用 if constexpr 触发
template<typename T>
T process(T v) {
if constexpr (compute_threshold() > 0) {
// compute_threshold() 被强制编译期调用
}
}
2.4 编译期求值的限制
| 限制类型 | 说明 | 解决方案 |
|---|---|---|
| 堆内存分配 | C++20 之前不可用 | 使用 std::array 或 C++23 std::vector |
| 虚函数调用 | 无法编译期解析 | 使用模板静态分发 |
| 异常处理 | C++23 之前不可用 | 返回错误码或使用 std::optional |
| 运行时输入 | 无法编译期确定 | 使用配置表或宏 |
| 过长计算 | 编译器超时 | 分段计算或移至运行期 |
3. 模板歧义消除
3.1 模板歧义的来源
当多个模板重载都匹配某个调用时,编译器会报错"ambiguous overload"。常见原因:
- ✗ 多个模板参数都有默认值
- ✗ 推导出的类型导致多个重载同时匹配
- ✗ 既有模板版本又有特化版本
3.2 使用 if constexpr 消除歧义
// 示例:处理多种容器的通用函数
template<typename T>
auto size(const T& container) {
// 使用 if constexpr 消除类型歧义
if constexpr (constexpr auto* ptr = dynamic_cast<const std::vector<*>>(&container)) {
return ptr->size();
} else if constexpr (constexpr auto* ptr = dynamic_cast<const std::list<*>>(&container)) {
return ptr->size();
} else if constexpr (constexpr auto* ptr = dynamic_cast<const std::array<*, *>>(&container)) {
return ptr->size();
} else {
return container.size();
}
}
⚠️ 错误:dynamic_cast 不能 constexpr
上面的例子使用了 dynamic_cast,但 dynamic_cast 只能在运行期使用。正确的 constexpr 版本需要使用类型特征检测。
3.3 使用类型特征消除歧义
// 正确的 constexpr 版本:使用类型特征
template<typename T>
constexpr size_t size(const T& container) {
if constexpr (std::is_array_v<T>) {
return std::extent_v<T>;
} else if constexpr (std::is_container_v<T>) { // 自定义特征
return container.size();
} else {
static_assert(std::is_container_v<T>, "not a container");
}
}
// 定义容器特征
template<typename T, typename = void>
struct is_container : std::false_type {};
template<typename T>
struct is_container<T, std::void_t<decltype(std::declval<T>().size())>>
: std::true_type {};
4. 容器实现
4.1 constexpr std::array (C++20)
C++20 使得 std::array 的绝大多数操作都可以 constexpr:
// C++20 constexpr std::array 示例
constexpr std::array<int, 5> fibonacci = []{
std::array<int, 5> arr = {};
arr[0] = 1;
arr[1] = 1;
for (size_t i = 2; i < 5; ++i) {
arr[i] = arr[i-1] + arr[i-2];
}
return arr;
}();
// 编译期访问
static_assert(fibonacci[4] == 5); // 编译期验证
4.2 constexpr std::vector (C++23)
C++23 终于支持 constexpr std::vector:
// C++23 constexpr std::vector
constexpr std::vector<int> make_vector(size_t n) {
std::vector<int> vec;
vec.reserve(n);
for (size_t i = 0; i < n; ++i) {
vec.push_back(static_cast<int>(i));
}
return vec;
}
// 注意:constexpr vector 仍然使用堆分配
// 但这些分配在编译期完成,不影响运行期性能
4.3 手写 constexpr 容器
// 一个简单的编译期向量实现(用于学习)
template<typename T, size_t N>
struct ct_vector {
T data_[N];
size_t size_ = 0;
constexpr ct_vector() = default;
constexpr void push_back(const T& value) {
data_[size_++] = value;
}
constexpr T& operator[](size_t i) { return data_[i]; }
constexpr const T& operator[](size_t i) const { return data_[i]; }
constexpr size_t size() const { return size_; }
};
// 使用示例
constexpr ct_vector<int, 100> vec = []{
ct_vector<int, 100> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
return v;
}();
static_assert(vec[2] == 3);
5. 应用案例
5.1 游戏配置表编译期加载
游戏配置表(如技能加成表、怪物属性表)通常在运行时解析。编译期加载可以完全消除这个开销:
// 游戏配置表 - 编译期解析
// 定义配置表结构
struct MonsterConfig {
constexpr MonsterConfig(string_view name, int hp, int atk)
: name_(name), hp_(hp), atk_(atk) {}
constexpr auto& name() const { return name_; }
constexpr auto hp() const { return hp_; }
constexpr auto atk() const { return atk_; }
private:
string_view name_;
int hp_;
int atk_;
};
// 编译期配置表
constexpr ct_vector<MonsterConfig, 128> MONSTERS = []{
ct_vector<MonsterConfig, 128> vec;
// 编译期解析 JSON/CSV 配置表
// 这里直接硬编码作为示例,实际可从文件读取
vec.push_back(MonsterConfig("Slime", 10, 2));
vec.push_back(MonsterConfig("Goblin", 50, 8));
vec.push_back(MonsterConfig("Dragon", 1000, 100));
return vec;
}();
// 编译期验证
static_assert(MONSTERS[2].hp() == 1000);
static_assert(MONSTERS[2].atk() == 100);
5.2 编译期 UUID 生成
// 编译期 UUID 生成(用于配置表 ID)
constexpr uint32_t hashcompile_time(string_view str) {
uint32_t hash = 0x811c9dc5;
for (auto c : str) {
hash ^= static_cast<uint32_t>(c);
hash *= 0x01000193;
}
return hash;
}
// 生成实体 ID
struct EntityId {
uint32_t value;
constexpr explicit EntityId(uint32_t v) : value(v) {}
constexpr EntityId(string_view name) : value(hashcompile_time(name)) {}
constexpr operator uint32_t() const { return value; }
};
// 使用示例
constexpr EntityId PLAYER_ID = "player";
constexpr EntityId ENEMY_ID = "enemy";
static_assert(PLAYER_ID != ENEMY_ID);
5.3 编译期查找表
// 编译期三角函数查找表
template<size_t N>
constexpr std::array<float, N> generate_sin_table(float max_angle = 360.0f) {
std::array<float, N> table = {};
for (size_t i = 0; i < N; ++i) {
constexpr float PI = 3.14159265358979f;
float angle = max_angle * static_cast<float>(i) / static_cast<float>(N);
table[i] = sinf(angle * PI / 180.0f);
}
return table;
}
constexpr auto SIN_TABLE = generate_sin_table<360>();
// 使用
constexpr float sin_30 = SIN_TABLE[30]; // 编译期计算,约 0.5
static_assert(sin_30 > 0.49f && sin_30 < 0.51f);
💡 性能收益
编译期计算对于游戏配置和查找表特别有价值:
- 消除运行期 JSON/CSV 解析开销
- 编译期验证配置正确性
- 运行时零计算成本
- 生成只读数据段,无堆分配
总结
C++17-23 的 constexpr 特性使得编译期计算越来越强大。从 constexpr if 的歧义消除,到 constexpr 容器的完整支持,我们可以在编译期完成过去需要运行时的计算。
✅ 快速决策
- ✓ 游戏配置表:编译期加载,运行期零开销
- ✓ 编译期查找表(sin/cos/物理常量):零运行期计算
- ✓ 类型特征和 SFINAE:使用 constexpr if 更清晰
- ✓ 复杂常量表达式:考虑是否值得编译器超时风险
- ✗ 频繁变化的配置(从网络拉取):保持运行期解析