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 的话,你得写一堆 decltype 和 std::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。如果传入 int 和 double,编译器会报错。这比泛型 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 只能推导出一种类型。传入 int 和 double 时,编译器无法确定 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 演进中的位置:
实际应用场景
模板 Lambda 在哪些地方特别好用?我总结了几点:
- STL 算法中的自定义操作:比如
std::sort的比较器,需要类型安全时用模板 Lambda。 - 类型转换工具:写一个通用的类型转换 Lambda,确保输入输出类型一致。
- 编译期类型检查:结合
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 触发
这里模板参数 T 和 U 分别代表目标类型和源类型。注意 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