目录

  1. constexpr 演进
  2. 编译期求值边界
  3. 模板歧义消除
  4. 容器实现
  5. 应用案例

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):

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 更清晰
  • ✓ 复杂常量表达式:考虑是否值得编译器超时风险
  • ✗ 频繁变化的配置(从网络拉取):保持运行期解析