22. Lambda 与 constexpr:C++17 起 Lambda 可在编译期求值

说实话,C++17 给 lambda 带来的这个能力,是我个人最期待的特性之一。

在 C++17 之前,lambda 表达式本质上是一个运行时的语法糖。你写一个 lambda,编译器帮你生成一个匿名函数对象,然后你在运行时调用它。但到了 C++17,情况变了——lambda 可以成为 constexpr,也就是说,它可以在编译期就被求值。

这意味着什么?意味着你可以用 lambda 去写那些原本需要模板元编程才能搞定的东西。我当年刚接触模板元编程时,被那些递归的 structenum 搞得头大。现在好了,一个 lambda 就能搞定。

22.1 核心规则:什么样的 lambda 是 constexpr?

C++17 规定:如果一个 lambda 的捕获列表和函数体满足 constexpr 函数的要求,那么这个 lambda 就可以在编译期求值。

具体来说,需要满足:

  • 没有 staticthread_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 的核心关系:

C++17 Lambda 与 constexpr 核心知识体系 constexpr Lambda 基本规则 默认就是 constexpr 满足 constexpr 函数要求 可显式声明 constexpr 捕获规则 捕获 constexpr 变量 捕获运行时变量 → 不可编译期求值 引用捕获需谨慎 应用场景 编译期算法(阶乘、斐波那契) STL 算法配合 替代模板元编程 泛型支持 auto 参数 + constexpr 支持自定义类型 编译期多态 注意事项 避免深度递归 注意编译时间 static_assert 验证 核心:编译期求值,运行时零开销

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