C++14起引入的泛型lambda表达式以及C++20起引入的模板lambda
C++11标准起就开始引入了一种现代化的编程语言语法特性——lambda表达式。随后,Java 8、JavaScript ES 6、Python等编程语言也都纷纷加入了这一特性。而对于C语言,Clang编译器的C语言语法扩展中也由Apple贡献了其创新性的Blocks语法特性。lambda表达式原本属于函数式编程语言的基本语法要素,而现代化的结构化编程语言中使用lambda表达式很多是为了提升函数功能的内聚性,并简化函数引用接口。比如,创建一个线程的函数例程、指定一个搜索算法的满足条件的谓词(predicate)例程等,这些都可以直接传入lambda表达式,而不需要在全局名字空间特地定义这么一个函数。
C++14起引入的泛型lambda表达式
为了能使lambda表达式能与C++普通函数一样拥有泛型功能,C++14中衍化了其类型推导系统,允许lambda表达式的形参类型用 auto 进行指定,使得该lambda表达式作为一个泛型表达式。下面我们来看一些例子:
#include <cstdio>
extern "C" auto CPPTest()
{
const struct
{
int a, b;
} s { 1, 2 };
// C++14起,lambda表达式的捕获列表中可以定义局部变量,
// 并可以用表达式对其赋值。
auto const lam1 = [x = s.a + 1, y = s.b - 1](auto a, auto b) {
return x + y + a - decltype(a)(b);
};
// 这里lam1的返回类型为int。
// 调用lam1之后,最后return语句的表达式为:
// return (1 + 1) + (2 - 1) + 100 - 48;
auto const a = lam1(100, u8'0');
// 输出:a = 55
printf("a = %d\n", a);
// 这里lam1的返回类型为double。
// 调用lam1之后,最后return语句的表达式为:
// return (1 + 1) + (2 - 1) + 10.5 - 2.5;
auto const b = lam1(10.5, 2.5f);
// 输出:b = 11
printf("b = %d\n", int(b));
}
上述代码中调用了两次“lam1”这一lambda表达式对象。我们看到,通过不同的参数类型,lam1所返回的类型也会有所不同。为了能更清晰地看到现代化C++编程语言相关IDE的智能提示,笔者下面再截一个Visual Studio 2022 Community Edition下的代码展示图:
通过上图我们可以看到,通过类型自动推导出的临时对象a和b的类型都能直接被IDE给提示出来。
C++20起引入的模板lambda表达式
稍微深入学过一点C++的朋友们应该知道,C++除了OOP之外,还有一大特性就是模板(template)。通过模板语法,有一个叫Erwin Unruh的老外还专门搞了一个叫“元编程”的概念。所以C++98中就已经能支持类、结构体、联合体的模板,还能支持函数模板。而到了C++14,变量也能支持模板了。而再到C++20,终于lambda表达式也终于能支持模板了。
那有些朋友可能会问,既然C++14中已经能支持泛型lambda了,那要模板lambda还有啥用?其实C++中的模板这玩意儿不仅仅提供了泛型机制,我们知道,C++中的模板实参不单单可以传类型,而且还能传递编译时的常量表达式!而元编程就是通过传递常量表达式这一模板特性来实现的。
模板lambda的语法形式为:
[ 「捕获列表」 ] < 「模板形参列表」 > ( 「函数形参列表」 ) { 「函数体」 } ;
而要调用模板lambda表达式则更为复杂一些,其语法形式为:
「lambda对象标识符」.template operator ( ) < 「模板实参列表」 > ( 「函数实参列表」 )
下面我们来举一个完整的例子:
#include <cstdio>
extern "C" auto CPPTest()
{
// 模板lambda表达式lam2具有两个模板形参,
// 第一个为类型;第二个为常量。
constexpr auto lam2 = []<typename T, T n>(const T &t) {
if constexpr (n < T(0)) {
puts("Illegal value!!");
}
return t + n;
};
constexpr auto a = lam2.template operator ()<int, 5>(10);
// 输出:a = 15
printf("a = %d\n", a);
constexpr auto b = lam2.template operator ()<double, 5.5>(4.5);
// 输出:b = 10
printf("b = %d\n", int(b));
}
同样,为了给大家提供类型推导之后的表现形式,这里再为大家提供Visual Studio 2022 Community Edition里的截图。
此外,C++23中还会引入lambda表达式的递归调用语法,而且Visual Studio 2022内置的MSVC++已经能支持了,只要在项目属性中将“C++语言标准”这一选项配置为“/std:c++latest”即可。这么一来,lambda表达式就能支持所有普通函数能干的活儿啦~下面可以看一下C++23是如何实现lambda表达式递归的:
#include <cstdio>
extern "C" auto CPPTest()
{
constexpr auto factorial = [](this auto &&lamSelf, const int& n) {
if (n < 0) puts("Inalid value!");
if (n == 0) return 1;
return n * lamSelf(n - 1);
};
auto const n = factorial(5);
// 输出:n = 120
printf("n = %d\n", n);
}
其实就是在lambda表达式形参列表的第一个参数以 this auto && identifier 这种形式给出。这么一来就可以在当前lambda表达式内部通过 identifier 来表明它引用的是当前的lambda对象本身了。当然,上述代码中的 this auto &&lamSelf 也可以用 this auto const &lamSelf 来替代,两者都没问题。