第六章:constexpr与编译期计算

constexpr 是 C++11 引入的关键字,到了 C++14/17/20 一路进化。说白了,它让「编译期干活」这件事变得非常自然。我刚开始接触时觉得它就是个加强版 const,后来才发现——这玩意儿能做的事远比你想象的多。

6.1 constexpr 函数与变量

先看最简单的用法:

constexpr int square(int x) {
    return x * x;
}

int main() {
    constexpr int val = square(5);  // 编译期就算好了
    int runtime = square(rand());   // 运行时才算
}

注意看,同一个函数既能编译期用,也能运行时用。这就是 constexpr 的妙处。我在项目中遇到过不少新人以为 constexpr 函数「只能」在编译期用,其实不是——它只是「可以」在编译期用。

核心规则:

  • constexpr 函数体在 C++14 后可以包含循环、分支、局部变量
  • 参数是编译期常量时,结果在编译期计算
  • 参数是运行时变量时,退化为普通函数调用

嗯,这里要注意:C++11 的 constexpr 函数体只能有一条 return 语句,非常蛋疼。C++14 放宽了这个限制,所以如果你还在用 C++11,建议升级。

6.2 编译期字符串处理

字符串处理也能在编译期做?当然可以。你想想看,很多字符串操作其实不依赖运行时状态。

constexpr size_t strlen_c(const char* s) {
    size_t len = 0;
    while (s[len] != '\0') ++len;
    return len;
}

constexpr bool is_palindrome(const char* s, size_t n) {
    for (size_t i = 0; i < n / 2; ++i) {
        if (s[i] != s[n - 1 - i]) return false;
    }
    return true;
}

int main() {
    constexpr auto len = strlen_c("hello");  // 5
    constexpr auto pal = is_palindrome("abcba", 5);  // true
}

我曾经在写一个嵌入式协议解析器时,把所有字符串比较都挪到了编译期。效果很明显——运行时零开销,而且 bug 在编译阶段就暴露了。

小技巧:编译期字符串处理最适合做哈希计算、长度校验、模式匹配。别用它做动态分配或 I/O 操作,那是不可能的。

6.3 constexpr 容器

C++20 之前,标准库容器不能在 constexpr 上下文中使用。但你可以自己写一个简单的编译期容器:

template<typename T, size_t N>
class ConstexprArray {
    T data[N] = {};
public:
    constexpr ConstexprArray() = default;
    
    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 N; }
    
    constexpr T* begin() { return data; }
    constexpr const T* begin() const { return data; }
    constexpr T* end() { return data + N; }
    constexpr const T* end() const { return data + N; }
};

constexpr int sum_array() {
    ConstexprArray<int, 5> arr;
    arr[0] = 1; arr[1] = 2; arr[2] = 3;
    arr[3] = 4; arr[4] = 5;
    
    int sum = 0;
    for (auto v : arr) sum += v;
    return sum;
}

static_assert(sum_array() == 15);

C++20 开始,std::vectorstd::string 的部分操作也支持 constexpr 了。我个人习惯还是用自定义的小容器,因为标准库的 constexpr 支持还不够稳定。

注意:constexpr 容器不能使用动态内存分配(除非在编译期分配并在编译期释放)。C++20 放宽了一些限制,但依然有坑。我建议你写 constexpr 容器时,优先用栈上数组。

6.4 if constexpr

if constexpr 是 C++17 引入的编译期分支。它和普通 if 最大的区别是:不满足条件的分支根本不会被实例化。

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;
    int* p = &x;
    
    auto v1 = get_value(x);  // 42
    auto v2 = get_value(p);  // 42
}

为什么会这样?因为 if constexpr 在编译期就决定了走哪条路。另一条路的代码根本不会出现在生成的二进制里。这在模板元编程中特别有用。

我曾经在写一个序列化库时,用 if constexpr 处理不同类型:

template<typename T>
void serialize(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        // 直接写二进制
        write_raw(&value, sizeof(T));
    } else if constexpr (std::is_same_v<T, std::string>) {
        // 先写长度,再写内容
        write_size(value.size());
        write_raw(value.data(), value.size());
    } else {
        // 递归序列化每个成员
        for_each_member(value, [](auto& member) {
            serialize(member);
        });
    }
}

嗯,这里要注意:if constexpr 的条件必须是编译期常量表达式。你不能写 if constexpr (x > 0) 这种,除非 x 是 constexpr 变量。

if constexpr 的典型应用场景:

  • 类型特征分发(替代 SFINAE)
  • 编译期算法选择
  • 模板代码的编译期剪枝
  • 减少模板实例化数量

6.5 知识体系总览

下面这张图总结了本章的核心内容:

constexpr 编译期计算 constexpr 函数与变量 编译期常量求值 运行时降级为普通函数 C++14 放宽函数体限制 编译期字符串处理 长度计算 / 哈希 回文 / 模式匹配 零运行时开销 constexpr 容器(C++20 增强) if constexpr 编译期分支选择 替代 SFINAE 减少模板实例化

这张图把 constexpr 的四个核心方向串起来了。你从上往下看,左边是函数与变量,中间是字符串处理,右边是 if constexpr,底部是容器。每个方向都有它的适用场景。

我的建议:刚开始用 constexpr 时,先从简单的函数做起。等你熟悉了编译期计算的思维模式,再尝试字符串处理和自定义容器。if constexpr 可以早点用,它比 SFINAE 好理解太多了。

constexpr 的核心价值在于:把运行时的工作提前到编译期。这不仅提升了性能,更重要的是——编译期能帮你发现更多 bug。我这些年写 C++ 最大的体会就是:能编译期做的事,绝不拖到运行时。


公众号:蓝海资料掘金营,微信deep3321