跳至内容

constexpr、consteval 与编译时编程

使用 constexpr、consteval 和 constinit 将计算从运行时移到编译时。构建查找表、验证配置,以及使用 constexpr 容器。

C++ 中的编译时编程

C++ 独特地允许你在编译时运行真正的代码。这意味着编译器可以计算值、验证输入、构建查找表,甚至运行复杂的算法 ── 所有这些都在程序发布之前完成。结果作为常量直接嵌入二进制文件中。

C++ 编译时编程的演进:

- C++11:引入 constexpr ── 简单的、单一 return 语句的函数
- C++14:放松的 constexpr ── 循环、局部变量、多条语句
- C++17:if constexpr ── 编译时分支,丢弃未采用的路径
- C++20:consteval(必须在编译时执行)、constinit(确保静态初始化在编译时完成)、constexpr 容器(vector、string)
- C++23:if consteval ── 检测是否处于常量求值上下文中

constexpr 函数

constexpr 函数可以在编译时或运行时求值。如果在需要常量的上下文中用编译时参数调用它(模板参数、constexpr 变量、static_assert、数组大小),编译器会在编译时对其求值。否则,它像普通函数一样在运行时运行。

constexpr_functions.cpp
#include <iostream>
#include <array>
#include <cstdint>

constexpr int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

constexpr int fibonacci(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1;
    for (int i = 2; i <= n; ++i) {
        int next = a + b;
        a = b;
        b = next;
    }
    return b;
}

int main() {
    // Compile-time evaluation: used in constexpr context
    constexpr int fact10 = factorial(10);  // computed at compile time
    static_assert(fact10 == 3628800);

    constexpr int fib10 = fibonacci(10);   // computed at compile time
    static_assert(fib10 == 55);

    // Compile-time array sizes
    std::array<int, factorial(5)> arr{};   // array of 120 elements

    // Runtime evaluation: argument not constexpr
    int n;
    std::cin >> n;
    std::cout << "factorial(" << n << ") = " << factorial(n) << '\n';
    // Same function, now runs at runtime
}

consteval 与 constinit(C++20)

consteval 标记一个必须在编译时求值的函数 ── 用运行时值调用它会产生编译错误。这称为「立即函数」。constinit 确保具有静态或线程局部存储期的变量在编译时初始化,防止「静态初始化顺序灾难」── 但与 constexpr 不同,该变量可以在初始化后被修改。

consteval_constinit.cpp
#include <iostream>

// consteval: MUST be evaluated at compile time
consteval int square(int x) {
    return x * x;
}

// constinit: must be initialized at compile time,
// but can be modified at runtime
constinit int global_value = square(7);  // 49, computed at compile time

// constexpr variable: initialized at compile time AND immutable
constexpr int fixed_value = square(10);  // 100, compile time, cannot change

int main() {
    constexpr int a = square(5);  // OK: compile-time context
    static_assert(a == 25);

    // int x = 5;
    // int b = square(x);  // ERROR: consteval requires compile-time args

    // constinit variable can be modified after initialization
    global_value = 100;  // OK: runtime modification allowed
    std::cout << global_value << '\n'; // 100

    // fixed_value = 200;  // ERROR: constexpr variable is const
}

if constexpr 实践

if constexpr(C++17)在编译时求值条件并完全丢弃未采用的分支。被丢弃的分支不会被实例化 ── 它不需要对给定的模板参数是有效代码。这与普通 if 有根本区别,普通 if 中两个分支都必须有效。

关键规则:

1. 条件必须是编译时常量表达式。
2. 在模板中,被丢弃的分支不会为当前模板参数实例化。
3. 在模板外部,两个分支仍然必须语法有效(只是不会执行)。
4. if constexpr 可以出现在普通函数中,不仅限于模板 ── 但丢弃仅在模板实例化中生效。
5. 每个 if constexpr 分支有自己的作用域 ── 在一个分支中声明的变量在另一个分支中不可见。

constexpr 容器(C++20)

C++20 允许 std::vector 和 std::string 在 constexpr 上下文中使用。你可以在编译时构建、操作和查询容器 ── 这是编译时编程能力的巨大扩展。约束是:瞬态分配 ── 在编译时求值期间分配的内存必须在 constexpr 求值结束前释放。

constexpr_containers.cpp
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
#include <array>

// Build and process a vector at compile time (C++20)
constexpr int sum_of_squares(int n) {
    std::vector<int> v;
    for (int i = 1; i <= n; ++i) {
        v.push_back(i * i);
    }
    return std::accumulate(v.begin(), v.end(), 0);
    // vector memory freed here — transient allocation
}

// Compile-time string processing
constexpr std::size_t count_vowels(std::string_view sv) {
    std::size_t count = 0;
    for (char c : sv) {
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||
            c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U') {
            ++count;
        }
    }
    return count;
}

// Generate a lookup table at compile time
template <std::size_t N>
constexpr auto generate_squares() {
    std::array<int, N> result{};
    for (std::size_t i = 0; i < N; ++i) {
        result[i] = static_cast<int>(i * i);
    }
    return result;
}

static_assert(sum_of_squares(5) == 55); // 1+4+9+16+25
static_assert(count_vowels("Hello World") == 3);

constexpr auto squares = generate_squares<10>();
static_assert(squares[7] == 49);

实用的编译时编程

编译时计算不仅仅是花哨的把戏。实际用途包括:构建嵌入二进制文件的查找表、在编译时验证配置、计算哈希值用于编译时字符串匹配,以及生成优化的代码路径。

practical_constexpr.cpp
#include <array>
#include <cstdint>
#include <stdexcept>

// Compile-time CRC32 table generation
constexpr auto generate_crc32_table() {
    std::array<uint32_t, 256> table{};
    for (uint32_t i = 0; i < 256; ++i) {
        uint32_t crc = i;
        for (int j = 0; j < 8; ++j) {
            crc = (crc >> 1) ^ (0xEDB88320 * (crc & 1));
        }
        table[i] = crc;
    }
    return table;
}

constexpr auto crc32_table = generate_crc32_table();

// Compile-time configuration validation
struct Config {
    int port;
    int max_connections;
    int timeout_ms;
};

consteval Config validated_config(int port, int max_conn, int timeout) {
    if (port < 1 || port > 65535)
        throw "Port out of range";  // compile error if invalid!
    if (max_conn < 1 || max_conn > 10000)
        throw "Invalid max connections";
    if (timeout < 0)
        throw "Negative timeout";
    return {port, max_conn, timeout};
}

// This validates at compile time — typo or bad value = build fails
constexpr auto config = validated_config(8080, 1000, 5000); // OK
// constexpr auto bad = validated_config(99999, 1000, 5000); // ERROR!
常见陷阱

编译时编程的常见错误:

1. 假设 constexpr 意味着仅在编译时执行 ── constexpr 函数也可以在运行时运行。只有 consteval 保证编译时求值。
2. 瞬态分配 ── 在 C++20 constexpr 上下文中,求值期间分配的所有内存必须在求值完成前释放。你不能将 constexpr std::vector 存储为全局变量(其堆内存会持久存在)。
3. 并非所有函数都是 constexpr 安全的 ── I/O、线程创建、reinterpret_cast 和内联汇编不能出现在 constexpr 上下文中。
4. 过度使用 constexpr ── 在编译时计算所有东西会增加构建时间。将其保留给真正受益于成为编译时常量的值。

关键要点
  • constexpr 函数根据上下文在编译时或运行时运行
  • consteval(C++20)强制仅在编译时求值 ── 运行时参数是编译错误
  • constinit(C++20)确保静态/线程局部变量在编译时初始化,但允许运行时修改
  • if constexpr 丢弃未采用的分支,该分支不需要对给定的模板参数有效
  • C++20 允许 std::vector 和 std::string 在 constexpr 上下文中使用,需遵守瞬态分配规则
  • 实际用途包括查找表生成、配置验证和编译时哈希

测验 — 检验你的知识

(15 经验值)

1. `constexpr` 和 `consteval` 之间有什么区别?

2. 在 C++20 中,你能否将 `constexpr std::vector<int>` 作为全局变量?

3. `constinit` 保证什么?