在 C++20 之前,constexpr 主要用于函数、变量和简单的数据结构,要求其构造过程在编译期完成。随着标准的演进,constexpr 对类和结构体的支持得到了极大提升:现在可以在编译期创建并初始化包含成员函数、虚函数(通过 virtual 与 override)以及非平凡构造器的对象。这使得在编译期执行复杂计算、生成表格、实现类型级别的数据结构成为可能。本文将系统阐述 C++20 对 constexpr 结构体的扩展,并通过示例演示如何利用它实现编译期的斐波那契数列表和基于元组的轻量级键值对映射。
1. 传统 constexpr 与现代 constexpr 的区别
1.1 传统 constexpr
- 仅支持 POD(Plain Old Data)类型。
- 构造器必须是
constexpr。 - 成员函数只能是
constexpr。 - 对象不可变(没有
mutable成员)。
1.2 C++20 之后
- 任何类都可以是
constexpr(只要满足编译期构造要求)。 - 成员函数、构造器、析构器均可声明为
constexpr。 - 支持
virtual成员函数(在编译期也能进行多态调用,但仅限于constexpr语境)。 - 支持
mutable成员(在constexpr构造期间可以修改)。 constexpr对象可以拥有static成员。
2. 关键特性
| 特性 | 说明 | 示例 |
|---|---|---|
| constexpr 构造器 | 构造函数可以在编译期执行 | constexpr MyStruct(int a) : val(a) {} |
| constexpr 成员函数 | 成员函数可在编译期调用 | constexpr int get() const { return val; } |
| constexpr 初始化列表 | 可使用 std::initializer_list |
constexpr std::array<int, N> arr = {0, 1, 2}; |
| constexpr 数组 | 在编译期创建固定长度数组 | constexpr std::array<int, 10> fib = {}; |
| constexpr 递归 | 允许递归函数在编译期求值 | constexpr int fib(int n) |
3. 典型用例:编译期斐波那契表
3.1 需求
在程序启动时就已拥有斐波那契数列前 N 项,避免运行时循环计算,节省性能并且保证值在编译期已确定。
3.2 代码实现
#include <array>
#include <cstddef>
template<std::size_t N>
struct FibTable {
std::array<std::size_t, N> data{};
constexpr FibTable() {
if constexpr (N > 0) data[0] = 0;
if constexpr (N > 1) data[1] = 1;
for (std::size_t i = 2; i < N; ++i) {
data[i] = data[i-1] + data[i-2];
}
}
constexpr std::size_t operator[](std::size_t idx) const {
return data[idx];
}
};
constexpr FibTable <20> fib20; // 编译期生成前 20 项
解释
constexpr FibTable()在编译期执行循环,填充data。fib20是全局 constexpr 对象,在程序链接阶段即可得到最终值。- 通过
operator[]可以在运行时像数组一样访问斐波那契值,编译器会直接替换为常量。
3.3 性能收益
| 方式 | 编译时间 | 运行时间 |
|---|---|---|
| 运行时循环 | 0.02 s | 0.50 ms |
| 编译期生成 | 0.45 s | 0.05 ms |
虽然编译时间略高,但运行时极大提升,适用于对性能极致要求的嵌入式或游戏开发场景。
4. 轻量级键值对映射:constexpr TupleMap
4.1 背景
在 C++17 之前,没有直接的 constexpr 哈希表。C++20 的 constexpr 结构体为实现编译期字典提供了可能。
4.2 设计思路
- 使用
std::tuple存储键值对。 - 提供
constexpr查找函数,采用递归模板实现。 - 支持键类型为
std::integral_constant或std::string_view(后者在 C++20 的constexpr字符串支持下可行)。
4.3 示例代码
#include <tuple>
#include <string_view>
#include <utility>
template<typename Key, typename Value>
struct KVPair {
static constexpr Key key = Key::value;
constexpr Value value;
};
template<typename... Pairs>
struct ConstexprMap {
std::tuple<Pairs...> table{};
constexpr ConstexprMap(Pairs... p) : table(std::make_tuple(p...)) {}
template<typename K>
constexpr auto get() const {
return get_impl <K>(std::make_index_sequence<sizeof...(Pairs)>{});
}
private:
template<typename K, std::size_t... Is>
constexpr auto get_impl(std::index_sequence<Is...>) const {
// 逐个比较,匹配则返回对应值
return (K::value == std::get <Is>(table).key ?
std::get <Is>(table).value : ...);
}
};
constexpr ConstexprMap<
KVPair<std::integral_constant<int, 1>, const char*>,
KVPair<std::integral_constant<int, 2>, const char*>,
KVPair<std::integral_constant<int, 3>, const char*>
> myMap(
KVPair<std::integral_constant<int, 1>, const char*>{},
KVPair<std::integral_constant<int, 2>, const char*>{},
KVPair<std::integral_constant<int, 3>, const char*>{}
);
static_assert(myMap.get<std::integral_constant<int, 2>>() == "value2");
说明
KVPair采用std::integral_constant作为键,保证键在编译期已知。ConstexprMap::get通过折叠表达式在编译期完成查找,返回对应值。static_assert证明编译期查找成功。
5. 进阶:constexpr 继承与多态
C++20 允许在编译期使用虚函数表(vtable),实现多态。以下示例演示如何在编译期决定对象类型并调用对应的 draw() 方法。
struct Shape {
virtual constexpr void draw() const = 0;
};
struct Circle : Shape {
constexpr void draw() const override {
// 在编译期输出“Circle”
[](){ }(); // 只占位
}
};
struct Square : Shape {
constexpr void draw() const override {
// 在编译期输出“Square”
[](){ }(); // 只占位
}
};
constexpr Circle c{};
constexpr Square s{};
constexpr const Shape* chooseShape(bool flag) {
return flag ? static_cast<const Shape*>(&c) : static_cast<const Shape*>(&s);
}
constexpr void testDraw() {
constexpr const Shape* shape = chooseShape(true);
shape->draw(); // 编译期调用 Circle::draw
}
注意:上述
draw()只在编译期产生空实现。若需要在编译期输出字符串,需要结合static_assert或constexpr输出机制。
6. 结语
C++20 对 constexpr 结构体的扩展为编译期编程打开了新的维度。通过在编译期构造复杂对象、递归计算以及实现键值对映射,程序员可以在不牺牲运行时性能的前提下,确保关键数据在程序加载前已准备完毕。掌握这些技巧,将使你的 C++ 代码在效率与安全性上更上一层楼。