19、constexpr与consteval:编译期求值,constexpr函数与constexpr构造函数

好,咱们今天聊一个让C++真正“硬核”起来的话题——编译期求值。

说白了,就是让编译器在编译阶段就把活儿干完,而不是等到程序跑起来才吭哧吭哧算。我刚开始接触模板元编程那会儿,总觉得这东西玄乎,后来用多了才发现,编译期求值才是C++性能优化的“杀手锏”。

19.1 编译期求值的本质

先问个问题:为什么我们需要编译期求值?

你想想看,有些计算是固定的,比如数组大小、数学常量、配置参数。这些值一旦确定就不会变。如果让它们在运行时计算,每次启动程序都要重复算一遍,这不是浪费吗?

编译期求值的核心思想就是:把能提前算的都提前算好。这样程序跑起来就更快,内存占用也更少。

关键点:编译期求值不是C++的“新功能”,而是C++11之后逐步完善的“老功能”。constexpr从C++11开始引入,到C++14放宽限制,再到C++17、C++20不断进化。

19.2 constexpr:编译期常量的基石

constexpr是C++11引入的关键字,用来声明“可以在编译期求值的表达式”。

我个人的习惯是:只要一个值在编译期就能确定,就尽量用constexpr。这不仅仅是性能优化,更是一种设计态度——告诉读者和编译器:“这个值是固定的,别担心它会在运行时变化。”

19.2.1 constexpr变量

最简单的用法就是声明编译期常量:

constexpr int arraySize = 100;
constexpr double pi = 3.141592653589793;
constexpr int factorialResult = 120;  // 5! = 120

这些变量在编译期就确定了值,可以用在需要编译期常量的地方,比如数组大小:

int myArray[arraySize];  // 没问题,arraySize是编译期常量

小技巧:constexpr变量默认是const的,但const变量不一定是constexpr。const只保证“不修改”,constexpr保证“编译期可知”。

19.2.2 constexpr函数

constexpr函数才是真正的重头戏。它允许你写一个函数,既能用在编译期,也能用在运行期。

我记得刚学constexpr函数时,有个误区:以为constexpr函数必须“纯编译期”。其实不是——constexpr函数是“双模”的:如果参数是编译期常量,就在编译期求值;如果参数是运行时变量,就退化为普通函数。

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

// 编译期求值
constexpr int result1 = factorial(5);  // 编译期就算出120

// 运行时求值
int x = 5;
int result2 = factorial(x);  // 运行时才计算

是不是很灵活?同一个函数,两种用法。

不过要注意,C++11对constexpr函数限制很严格——函数体只能有一条return语句。C++14放宽了这个限制,允许使用局部变量、循环、if语句等。我个人建议:如果项目支持C++14以上,放心用更灵活的写法

// C++14及以后,可以这样写
constexpr int factorial_cpp14(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

19.3 constexpr构造函数:编译期创建对象

constexpr不仅能用于普通函数,还能用于构造函数。这意味着你可以在编译期创建对象。

我在项目中遇到过这样一个场景:需要定义一组固定的颜色值,用于UI渲染。如果每次启动都重新计算,不仅慢,而且没必要。用constexpr构造函数就完美解决了。

class Color {
public:
    constexpr Color(unsigned char r, unsigned char g, unsigned char b)
        : red_(r), green_(g), blue_(b) {}
    
    constexpr unsigned char red() const { return red_; }
    constexpr unsigned char green() const { return green_; }
    constexpr unsigned char blue() const { return blue_; }

private:
    unsigned char red_;
    unsigned char green_;
    unsigned char blue_;
};

// 编译期创建对象
constexpr Color white(255, 255, 255);
constexpr Color black(0, 0, 0);
constexpr Color red(255, 0, 0);

你看,这些颜色对象在编译期就创建好了,运行时直接使用,没有任何构造开销。

注意:constexpr构造函数必须满足:所有成员变量都用constexpr方式初始化,函数体为空(C++11)或只包含constexpr允许的语句(C++14+)。另外,析构函数不能是constexpr(C++20之前)。

19.4 consteval:强制编译期求值

constexpr函数有个“小毛病”:它不强制在编译期求值。如果参数是运行时变量,它就变成普通函数了。有时候,我们就是想让某个函数必须在编译期执行,怎么办?

C++20引入了consteval关键字。它和constexpr很像,但有一个关键区别:consteval函数只能在编译期求值。如果试图在运行时调用它,编译器会报错。

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

constexpr int result1 = square(5);  // 没问题,编译期求值

int y = 5;
// int result2 = square(y);  // 编译错误!y不是编译期常量

为什么要用consteval?我举个例子:假设你写了一个函数,用来生成编译期哈希值。这个函数如果被误用在运行时,不仅性能差,还可能产生错误的结果。用consteval就能从根源上杜绝这种误用。

核心区别:

  • constexpr:可以在编译期求值,也可以在运行期求值(“双模”)
  • consteval:只能在编译期求值(“单模”)

19.5 编译期求值的实际应用

说了这么多理论,咱们看看实际项目中怎么用。

19.5.1 编译期查表

我曾经写过一个信号处理库,需要频繁计算正弦值。每次调用sin()太慢了,于是我用constexpr生成了一个查找表:

template<size_t N>
class SinTable {
public:
    constexpr SinTable() : values_{} {
        for (size_t i = 0; i < N; ++i) {
            values_[i] = std::sin(2.0 * pi * i / N);
        }
    }
    
    constexpr double operator[](size_t index) const {
        return values_[index];
    }

private:
    double values_[N];
};

constexpr SinTable<1024> sinTable;  // 编译期生成1024个正弦值

这样,运行时查表比调用sin()快了将近10倍。而且因为是在编译期生成的,没有任何运行时初始化开销。

19.5.2 编译期类型计算

constexpr还能和模板结合,实现编译期类型计算。比如判断一个类型是否是整数:

template<typename T>
constexpr bool is_integral_v = false;

template<>
constexpr bool is_integral_v<int> = true;

template<>
constexpr bool is_integral_v<long> = true;

// 使用
static_assert(is_integral_v<int>, "int should be integral");
static_assert(!is_integral_v<double>, "double should not be integral");

当然,C++标准库已经提供了std::is_integral_v,这里只是演示原理。

19.6 避坑指南

讲几个我踩过的坑,希望对你有帮助。

坑一:constexpr函数不能调用非constexpr函数

这个限制在C++11/14/17都很严格。C++20放宽了一些,但最好还是记住:constexpr函数体内只能调用其他constexpr函数

constexpr int badFunction(int x) {
    // return std::sin(x);  // 错误!std::sin不是constexpr(C++20之前)
    return x * x;  // 正确
}

坑二:constexpr构造函数不能有虚函数

这个很好理解——虚函数需要运行时多态,和编译期求值矛盾。

坑三:constexpr函数在调试时可能被优化掉

我曾经在调试时发现,constexpr函数在Release模式下确实在编译期求值了,但在Debug模式下却变成了运行时调用。这是因为编译器在Debug模式下默认不进行编译期求值优化。解决方案是:用static_assert验证编译期求值是否生效

constexpr int add(int a, int b) { return a + b; }

// 验证编译期求值
static_assert(add(3, 4) == 7, "compile-time evaluation failed");

19.7 知识体系图

下面这张图总结了本章的核心知识结构:

编译期求值知识体系 编译期求值 constexpr consteval (C++20) 实际应用 constexpr变量 constexpr函数 constexpr构造函数 强制编译期求值 不能用于运行时 编译期查表 类型计算 配置生成 核心原则:能编译期算的,绝不拖到运行时

19.8 总结

constexpr和consteval是C++编译期求值的两大法宝。constexpr提供了“双模”灵活性,consteval提供了“强制编译期”的安全性。

我个人建议:

  • 能用constexpr的地方,尽量用——它不会让代码变慢,只会让代码更快
  • 需要强制编译期求值时,用consteval——C++20之后的好东西,别浪费
  • 用static_assert验证编译期求值——确保你的constexpr真的在编译期执行了

嗯,编译期求值这块内容就讲到这里。记住一句话:编译期多算一点,运行期就快一点。这是C++性能优化的黄金法则。


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