22. Lambda 与 constexpr:C++17 起 Lambda 可在编译期求值
说实话,C++17 给 lambda 带来的这个能力,是我个人最期待的特性之一。
在 C++17 之前,lambda 表达式本质上是一个运行时的语法糖。你写一个 lambda,编译器帮你生成一个匿名函数对象,然后你在运行时调用它。但到了 C++17,情况变了——lambda 可以成为 constexpr,也就是说,它可以在编译期就被求值。
这意味着什么?意味着你可以用 lambda 去写那些原本需要模板元编程才能搞定的东西。我当年刚接触模板元编程时,被那些递归的 struct 和 enum 搞得头大。现在好了,一个 lambda 就能搞定。
22.1 核心规则:什么样的 lambda 是 constexpr?
C++17 规定:如果一个 lambda 的捕获列表和函数体满足 constexpr 函数的要求,那么这个 lambda 就可以在编译期求值。
具体来说,需要满足:
- 没有
static或thread_local变量声明 - 没有虚函数调用
- 没有
try/catch块 - 没有
goto语句 - 所有被调用的函数都必须是
constexpr - 捕获的变量必须是
constexpr或字面量类型
嗯,其实这些限制和 constexpr 函数是一样的。说白了,只要你的 lambda 能在编译期算出来,编译器就会尝试在编译期算它。
关键点:从 C++17 开始,lambda 默认就是 constexpr 的,只要它满足条件。你不需要显式写 constexpr 关键字,但写了也没错。
22.2 基础示例:编译期计算
来看一个最简单的例子:
// C++17 起,这个 lambda 可以在编译期求值
auto square = [](int x) { return x * x; };
// 编译期求值
constexpr int result = square(5); // result = 25,编译期就算好了
// 运行时求值
int n = 10;
int runtime_result = square(n); // 运行时才算
你想想看,同样的代码,既能在编译期用,又能在运行时用。这就是 lambda 的灵活性。
我在项目中遇到过这样的情况:有一个配置表,里面有些值是固定的,有些是运行时才能确定的。用 constexpr lambda,我可以在编译期就把固定值算好,运行时只处理动态部分。性能提升很明显。
22.3 显式声明 constexpr lambda
虽然 C++17 默认让 lambda 成为 constexpr,但有时候你可能会想显式声明一下。为什么?
- 代码可读性:告诉读者「这个 lambda 是设计用来编译期计算的」
- 强制检查:如果你写了
constexpr但 lambda 不满足条件,编译器会报错
// 显式声明 constexpr lambda
auto add = [](int a, int b) constexpr { return a + b; };
constexpr int sum = add(3, 4); // OK,编译期求值
// 如果 lambda 不满足 constexpr 条件,编译器会报错
// auto bad = [](int x) constexpr {
// static int counter = 0; // 错误!static 变量不允许
// return x + counter++;
// };
我的建议:如果你明确知道这个 lambda 要在编译期使用,就加上 constexpr。这就像给代码加了一个「编译期契约」,让编译器帮你把关。
22.4 捕获 constexpr 变量
lambda 可以捕获 constexpr 变量,然后在编译期使用它们。这一点非常有用。
constexpr int factor = 10;
// 捕获 constexpr 变量
auto multiply = [factor](int x) { return x * factor; };
constexpr int result = multiply(5); // 50,编译期求值
但要注意:如果你捕获的是运行时变量,那这个 lambda 就不能在编译期求值了。
int runtime_factor = 10; // 运行时变量
auto multiply = [runtime_factor](int x) { return x * runtime_factor; };
// constexpr int result = multiply(5); // 错误!不能在编译期求值
int result = multiply(5); // OK,运行时求值
为什么会这样?因为 constexpr 要求所有输入在编译期都是已知的。你捕获了一个运行时变量,编译器在编译期根本不知道它的值,自然没法算。
22.5 泛型 constexpr lambda
C++14 引入了泛型 lambda(用 auto 做参数),C++17 让它可以和 constexpr 结合。这就厉害了——你可以写出一个在编译期工作的泛型函数。
// 泛型 constexpr lambda
auto square = [](auto x) { return x * x; };
constexpr int int_result = square(5); // 25
constexpr double dbl_result = square(3.14); // 9.8596
// 甚至可以用于自定义类型
struct Point {
int x, y;
constexpr Point(int a, int b) : x(a), y(b) {}
constexpr int area() const { return x * y; }
};
constexpr Point p(3, 4);
constexpr int area = square(p.area()); // 144
我记得有一次写一个数学库,需要支持不同类型(int、float、自定义的 FixedPoint 类)的平方运算。用泛型 constexpr lambda,一行代码搞定,而且编译期就能算出常量表。要是用模板,得写一堆特化。
22.6 编译期算法:用 lambda 替代模板元编程
这是我最喜欢的用法。以前用模板元编程实现编译期算法,代码又长又难读。现在用 constexpr lambda,清晰多了。
// 编译期阶乘
constexpr auto factorial = [](int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
};
static_assert(factorial(5) == 120); // 编译期断言
// 编译期斐波那契
constexpr auto 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;
};
static_assert(fibonacci(10) == 55);
避坑指南:我曾经在项目里用 constexpr lambda 写了一个复杂的编译期排序算法。结果编译时间从 10 秒飙到了 3 分钟。后来发现,编译器在编译期递归展开时,生成了大量的模板实例化代码。
所以我的建议是:编译期计算适合小规模、确定性的算法。如果数据量很大,或者递归深度很深,还是放到运行时吧。
22.7 constexpr lambda 与 STL 算法
STL 算法配合 constexpr lambda,可以在编译期处理数据。这在 C++17 中特别有用,因为很多 STL 算法本身也变成了 constexpr。
#include <algorithm>
#include <array>
constexpr std::array<int, 5> data = {5, 3, 1, 4, 2};
// 编译期排序
constexpr auto sorted = [&data]() {
std::array<int, 5> result = data;
std::sort(result.begin(), result.end());
return result;
}();
static_assert(sorted[0] == 1);
static_assert(sorted[4] == 5);
// 编译期查找
constexpr auto it = std::find(data.begin(), data.end(), 3);
static_assert(*it == 3);
你想想看,以前这些操作只能在运行时做。现在编译期就能把数据排好序,运行时直接拿来用。对于嵌入式系统或者性能敏感的场景,这简直是福音。
22.8 知识体系图
下面这张图总结了 C++17 中 lambda 与 constexpr 的核心关系:
22.9 实战:编译期配置表
最后分享一个我在实际项目中用过的例子。我们需要一个配置表,里面有些值是固定的,有些是运行时才能确定的。
// 编译期配置表
constexpr auto make_config = []() {
// 编译期计算固定值
constexpr int base = 100;
constexpr int multiplier = 3;
// 返回一个结构体
struct Config {
int fixed_value;
int computed_value;
int (*runtime_func)(int); // 运行时函数指针
};
return Config{
.fixed_value = base,
.computed_value = base * multiplier, // 编译期就算好了
.runtime_func = nullptr // 运行时再赋值
};
};
constexpr auto config = make_config();
// config.fixed_value = 100, config.computed_value = 300
// 这些值在编译期就确定了,运行时直接使用
这样做的好处是:编译期就把能算的都算好,运行时只处理动态部分。对于性能敏感的场景,这种「编译期预处理 + 运行时轻量执行」的模式非常实用。
总结一下:C++17 的 constexpr lambda 让编译期编程变得前所未有的简单。你不用再写那些晦涩的模板元编程代码,一个 lambda 就能搞定。但也要注意,编译期计算不是万能的,该运行时做的事情还是交给运行时。
公众号:蓝海资料掘金营,微信deep3321