24. 模板 Lambda:C++20 引入,显式模板参数列表

好,咱们来聊聊 C++20 里一个让我眼前一亮的新特性——模板 Lambda。

说白了,就是 Lambda 表达式现在可以有自己的模板参数列表了。你想想看,在 C++14 里我们有了泛型 Lambda,用 auto 参数搞定一切。但有时候,auto 不够用,或者不够精确。这时候,模板 Lambda 就派上用场了。

为什么需要模板 Lambda?

先回忆一下 C++14 的泛型 Lambda:

auto lambda = [](auto x, auto y) { return x + y; };

这个 Lambda 能接受任意类型的参数。但有个问题——auto 推导出来的类型,你没法在 Lambda 内部显式地指定模板参数。比如你想让两个参数的类型不同,或者你想用 std::is_same_v 做类型检查,泛型 Lambda 就有点力不从心了。

我在项目中遇到过这样一个场景:需要写一个通用的比较器,但要求两个参数的类型必须一致。用泛型 Lambda 的话,你得写一堆 decltypestd::enable_if,代码又臭又长。嗯,C++20 的模板 Lambda 就是来解决这个痛点的。

基本语法

模板 Lambda 的语法很直观,就是在 Lambda 的捕获列表后面、参数列表前面,加上 template<typename T> 或者 template<typename... Args>

auto lambda = []<typename T>(T a, T b) {
    return a + b;
};

你看,这里明确要求两个参数的类型相同,都是 T。如果传入 intdouble,编译器会报错。这比泛型 Lambda 的 auto 要严格得多。

核心区别:

  • 泛型 Lambda:[](auto x, auto y) —— 每个 auto 独立推导
  • 模板 Lambda:[]<typename T>(T x, T y) —— 强制类型一致

实战:类型安全的比较器

我个人习惯在需要类型安全的场景下使用模板 Lambda。比如写一个通用的 max 函数:

auto safe_max = []<typename T>(T a, T b) -> T {
    return a > b ? a : b;
};

// 正确用法
int result = safe_max(3, 5);       // OK,都是 int
double result2 = safe_max(3.14, 2.71); // OK,都是 double

// 错误用法
// auto result3 = safe_max(3, 3.14); // 编译错误!类型不一致

为什么会这样?因为模板参数 T 只能推导出一种类型。传入 intdouble 时,编译器无法确定 T 到底是 int 还是 double,于是报错。这种严格性在某些场景下反而是好事——它帮你提前发现潜在的类型不匹配问题。

变参模板 Lambda

模板 Lambda 也支持变参模板。我记得有一次需要写一个通用的打印函数,参数个数不确定,类型也不确定。用变参模板 Lambda 就特别方便:

auto print_all = []<typename... Args>(Args... args) {
    (std::cout << ... << args) << std::endl;
};

print_all(1, "hello", 3.14); // 输出:1hello3.14

这里用了 C++17 的折叠表达式,配合变参模板 Lambda,一行代码搞定任意参数的打印。你想想看,要是用传统函数对象,得写多少模板代码?

与函数对象的对比

咱们来做个对比。传统函数对象实现同样的功能:

struct SafeMax {
    template<typename T>
    T operator()(T a, T b) const {
        return a > b ? a : b;
    }
};

SafeMax safe_max_obj;
int result = safe_max_obj(3, 5);

而模板 Lambda 的写法更简洁:

auto safe_max = []<typename T>(T a, T b) -> T {
    return a > b ? a : b;
};

说白了,模板 Lambda 就是函数对象的语法糖。但它的优势在于:

  • 就地定义:不需要单独声明一个结构体
  • 捕获方便:可以捕获外部变量
  • 代码更紧凑:适合一次性使用的场景

避坑指南

我曾经踩过的坑:

  • 模板 Lambda 不能显式指定模板参数,只能靠推导。也就是说,你不能写 lambda<int>(3, 5),只能写 lambda(3, 5) 让编译器自己推导。
  • 如果模板参数无法推导(比如没有出现在参数列表中),编译器会报错。所以模板参数最好都用在参数类型上。
  • 模板 Lambda 的模板参数列表不能有默认值。比如 []<typename T = int>(T x) 是不允许的。

知识体系图

下面这张图展示了模板 Lambda 在整个 C++ Lambda 演进中的位置:

C++ Lambda 演进路线 C++11 基本 Lambda C++14 泛型 Lambda (auto) C++20 模板 Lambda 三种 Lambda 对比 特性 C++11 基本 Lambda C++14 泛型 Lambda C++20 模板 Lambda 参数类型 固定类型 auto 推导 模板参数 类型安全 严格 宽松 可控 变参支持 不支持 有限 完整 代码简洁度 更高 最高 模板 Lambda 是泛型 Lambda 的增强版,提供更精确的类型控制

实际应用场景

模板 Lambda 在哪些地方特别好用?我总结了几点:

  1. STL 算法中的自定义操作:比如 std::sort 的比较器,需要类型安全时用模板 Lambda。
  2. 类型转换工具:写一个通用的类型转换 Lambda,确保输入输出类型一致。
  3. 编译期类型检查:结合 static_assert 在编译期做类型验证。

举个例子,写一个安全的类型转换 Lambda:

auto safe_cast = []<typename T, typename U>(U value) -> T {
    static_assert(std::is_arithmetic_v<T> && std::is_arithmetic_v<U>,
                  "Both types must be arithmetic");
    return static_cast<T>(value);
};

double d = safe_cast<double>(42);    // OK
// int i = safe_cast<int>("hello");  // 编译错误!static_assert 触发

这里模板参数 TU 分别代表目标类型和源类型。注意 T 没有出现在参数列表中,所以调用时必须显式指定 safe_cast<double>(42)。嗯,这也是模板 Lambda 的一个小限制——不能完全靠推导。

小技巧:

如果你想让模板 Lambda 既能推导又能显式指定,可以把所有模板参数都用在参数类型上。比如 []<typename T>(T x) 就比 []<typename T>(auto x) 更灵活。

总结

模板 Lambda 是 C++20 给 Lambda 表达式加上的「涡轮增压」。它让 Lambda 从「能用」变成了「好用」。我个人觉得,如果你还在用 C++14 的泛型 Lambda 做类型敏感的操作,是时候升级到 C++20 的模板 Lambda 了。

说白了,模板 Lambda 就是让你在 Lambda 里也能享受模板编程的全部威力——类型安全、变参支持、编译期检查。而且语法比传统函数对象简洁得多。嗯,这就是现代 C++ 的魅力所在。


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