C++11 Lambda 表达式

2022-12-02 15:55:10 浏览数 (1)

文章目录

  • 1.简介
    • 1.1定义
    • 1.2 作用
    • 1.3 语法格式
    • 1.4 调用方式
  • 2.Lambda 的捕获列表
  • 3.Lambda 的类型
  • 4.Lambda 的常量性和 mutable 关键字
  • 5.Lambda 的常见用法
  • 参考文献

1.简介

1.1定义

C 11 新增了很多特性,Lambda表达式(Lambda Expression)就是其中之一,很多语言都提供了 Lambda 表达式,如 Python,Java ,C# 等。本质上, Lambda 表达式是一个可调用的代码单元

^{[1]}

。实际上是一个闭包(closure),类似于一个匿名函数,拥有捕获所在作用域中变量的能力,能够将函数做为对象一样使用,通常用来实现回调函数、代理等功能。Lambda 表达式是函数式编程的基础,C 11 引入了 Lambda 则弥补了 C 在函数式编程方面的空缺。

1.2 作用

以往C 需要传入一个函数的时候,必须事先进行声明,视情况可以声明为一个普通函数然后传入函数指针,或者声明一个仿函数(functor,函数对象),然后传入一个对象。比如C 的STL中很多算法函数模板需要传入谓词(predicate)来作为判断条件,如排序算法sort。谓词就是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(unary predicate,只接受单一参数)和二元谓词(binary predicate,接受两个参数)。接受谓词的算法对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型。如下面使用sort()传入比较函数shorter()(这里的比较函数shorter()就是谓词)将字符串按长度由短至长排列。

代码语言:javascript复制
//谓词:比较函数,用来按长度排列字符串
bool shorter(const string& s1,const string& s2) {
	return s1.size()<s2.size();
}

// 按长度由短至长排列words
std::sort(words.begin(),words.end(),shorter);

Lambda表达式可以像函数指针、仿函数一样,作为一个可调用对象(callable object)被使用,比如作为谓词传入标准库算法。

也许有人会问,有了函数指针、函数对象为何还要引入Lambda呢?函数对象能维护状态,但语法开销大,而函数指针语法开销小,却没法保存函数体内的状态。如果你觉得鱼和熊掌不可兼得,那你可错了。Lambda函数结合了两者的优点,让你写出优雅简洁的代码。

1.3 语法格式

Lambda 表达式就是一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个Lambda具有一个返回类型、一个参数列表和一个函数体。但与内联函数不同,Lambda可以定义在函数内部,其语法格式如下:

代码语言:javascript复制
[capture list](parameter list) mutable(可选) 异常属性->return type{function body}

capture list(捕获列表)是一个Lambda所在函数中定义的局部变量的列表,通常为空,表示Lambda不使用它所在函数中的任何局部变量。parameter list(参数列表)、return type(返回类型)、function body(函数体)与任何普通函数基本一致,但是Lambda的参数列表不能有默认参数,且必须使用尾置返回类型。 mutable表示Lambda能够修改捕获的变量,省略了mutable,则不能修改。异常属性则指定Lambda可能会抛出的异常类型。

其中Lambda表达式必须的部分只有capture list和function body。在Lambda忽略参数列表时表示指定一个空参数列表,忽略返回类型时,Lambda可根据函数体中的代码推断出返回类型。例如:

代码语言:javascript复制
auto f = []{return 42;}

我们定义了一个可调用对象f,它不接受任何参数,返回42。auto关键字实际会将 Lambda 表达式转换成一种类似于std::function的内部类型(但并不是std::function类型,虽然与std::function“兼容”)。所以,我们也可以这么写:

代码语言:javascript复制
std::function<int()> Lambda = [] () -> int { return val * 100;};

如果你对std::function<int()>这种写法感到很神奇,可以查看 C 11 的有关std::function的用法。简单来说,std::function<int()>是一个实例化后的模板类,代表一个可调用的对象,接受 0 个参数,返回值是int。所以,当我们需要一个接受一个double作为参数,返回int的对象时,就可以写作:std::function<int(double)>

^{[3]}

1.4 调用方式

Lambda表达式的调用方式与普通函数的调用方式相同,上面Lambda表达式的调用方式如下:

代码语言:javascript复制
cout<<f()<<endl;  				//打印42

//或者直接调用
cout<<[]{return 42;}()<<endl;

我们还可以定义一个单参数的Lambda,实现上面字符串排序的 shorter() 比较函数的功能:

代码语言:javascript复制
auto f=[](cosnt string& a,const string& b) {
	return a.size()<b.size();
}

// 将Lambda传入排序算法sort中
sort(words.begin(),word2.end(),[](cosnt string& a,const string& b){
	return a.size()<b.size();
});

// 或者
sort(words.begin(),word2.end(),f);

2.Lambda 的捕获列表

Lambda可以获取(捕获)它所在作用域中的变量值,由捕获列表(capture list)指定在Lambda 表达式的代码内可使用的外部变量。比如虽然一个Lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些在捕获列表中明确指明的变量。Lambda在捕获所需的外部变量有两种方式:引用和值。我们可以在捕获列表中设置各变量的捕获方式。如果没有设置捕获列表,Lambda默认不能捕获任何的变量。捕获方式具体有如下几种:

代码语言:javascript复制
[] 不截取任何变量
[&} 截取外部作用域中所有变量,并作为引用在函数体中使用
[=] 截取外部作用域中所有变量,并拷贝一份在函数体中使用
[=,&valist]   截取外部作用域中所有变量,并拷贝一份在函数体中使用,但是对以逗号分隔valist使用引用
[&,valist] 以引用的方式捕获外部作用域中所有变量,对以逗号分隔的变量列表valist使用值的方式捕获
[valist] 对以逗号分隔的变量列表valist使用值的方式捕获
[&valist] 对以逗号分隔的变量列表valist使用引用的方式捕获
[this] 截取当前类中的this指针。如果已经使用了&或者=就默认添加此选项。

在[]中设置捕获列表,就可以在Lambda中使用变量a了,这里使用按值(=, by value)捕获。

代码语言:javascript复制
#include <iostream>

int main()
{
	int a = 123;
	auto lambda = [=]()->void
	{
		std::cout << "In Lambda: " << a << std::endl;
	};
	lambda();
	return 0;
}

编译运行结果如下:

代码语言:javascript复制
In Lambda: 123

按值传递到Lambda中的变量,默认是不可变的(immutable),如果需要在Lambda中进行修改的话,需要在形参列表后添加mutable关键字(按值传递无法改变Lambda外变量的值)。

代码语言:javascript复制
#include <iostream>
int main() {
	int a = 123;
	std::cout << a << std::endl;
	auto lambda = [=]() mutable ->void{
		a = 234;
		std::cout << "In Lambda: " << a << std::endl;
	};
    lambda();
	std::cout << a << std::endl;
	return 0;
}

编译运行结果为:

代码语言:javascript复制
123
In Lambda: 234  //可以修改
123             //注意这里的值,并没有改变

如果没有添加mutable,则编译出错:

代码语言:javascript复制
$ g   main.cpp -std=c  11
main.cpp:9:5: error: cannot assign to a variable captured by copy in a non-mutable Lambda 
				a = 234;
                ~ ^
1 error generated.

看到这,不禁要问,这魔法般的变量捕获是怎么实现的呢?原来,Lambda是通过创建个类来实现的。这个类重载了操作符(),一个Lambda函数是该类的一个实例。当该类被构造时,周围的变量就传递给构造函数并以成员变量保存起来,看起来跟函数对象(仿函数)很相似,但是 C 11 标准建议使用 Lambda 表达式,而不是函数对象,Lambda 表达式更加轻量高效,易于使用和理解

^{[4]}

3.Lambda 的类型

lambda函数的类型看起来和函数指针很像,都是把函数赋值给了一个变量。实际上,lambda函数是用仿函数实现的,它看起来又像是一种自定义的类。而事实上,lambda类型并不是简单的函数指针类型或者自定义类型,lambda函数是一个闭包(closure)的类,C 11标准规定,closure类型是特有的、匿名且非联合体的class类型。每个lambda表达式都会产生一个闭包类型的临时对象(右值)。因此,严格来说,lambda函数并非函数指针,但是C 11允许lambda表达式向函数指针转换,前提是没有捕捉任何变量且函数指针所指向的函数必须跟lambda函数有相同的调用方式。

代码语言:javascript复制
typedef int(*pfunc)(int x, int y);

int main() {
	auto func = [](int x, int y)->int {
		return x   y;
	};
	pfunc p1 = nullptr;
	p1 = func;					//lambda表达式向函数指针转换

	std::cout << p1(1, 2) << std::endl;

	return 0;
}

4.Lambda 的常量性和 mutable 关键字

C 11中,默认情况下 lambda 函数是一个 const 函数。按照规则,一个 const 成员函数是不能在函数体内改变非静态成员变量的值。

代码语言:javascript复制
int main() {
    int val = 0;
    auto const_val_lambda = [=] { val = 3; };    // 编译失败,不能在const的lambda函数中修改按值捕获的变量val

    auto mutable_val_lambda = [=]() mutable { val = 3; };

    auto const_ref_lambda = [&] { val = 3; };

    auto const_param_lambda = [](int v) { v = 3; };
    const_param_lambda(val);

    return 0;
}

阅读代码,注意以下几点: (1)可以看到在const的lambda函数中无法修改按值捕捉到的变量。lambda函数是通过仿函数来实现的,捕捉到的变量相当于是仿函数类中的成员变量,而lambda函数相当于是成员函数,const成员函数自然不能修改普通成员变量; (2)使用引用的方式捕获的变量在常量成员函数中值被更改则不会导致错误,其原因简单地说,由于const_ref_lambda 不会改变引用本身,而只会改变引用的值,所以编译通过; (3)使用mutable修饰的mutable_val_lambda,去除了const属性,所以可以修改按值方式捕获到的变量; (4)按值传递参数的const_param_lambda修改的是传入lambda函数的实参,当然不会有问题。

5.Lambda 的常见用法

(1)Lambda 函数和 STL Lambda 函数的引入为 STL 的使用提供了极大的方便。比如下面这个例子,当你想遍历一个 vector 的时候,原来你得这么写:

代码语言:javascript复制
vector<int> v={1,2,3,4,5,6,7,8,9};

//传统的for循环
for (auto itr = v.begin(), end = v.end(); itr != end; itr  ) {  
    cout << *itr;  
}

//函数指针
void printFunc(int v) {
	cout<<v;
}
for_each(v.begin(),v.end(),printFunc);

//仿函数
struct CPrintFunc {
	void operator() (int val)const { cout << val; }
};
for_each(v.begin(),v.end(),CPrintFunc());

现在有了 Lambda 函数你就可以这么写:

代码语言:javascript复制
for_each(v.begin(),v.end(),[](int val){  
    cout << val;
});

很明显,相比于传统的 for 循环、函数指针和仿函数,使用lambda函数更加简洁。如果处理vector成员的业务代码更加复杂,那么更能凸显 Lambda 函数的便捷。而且这么写之后执行效率反而会提高,因为编译器有可能使用循环展开来加速执行过程。

参考文献

Stanley B. Lippman著,王刚 杨巨峰译.C Primer中文版第五版.2013.P346-346 C 教程之Lambda表达式一 C 11 新特性:Lambda 表达式 深入理解C 11[M].7.3 lambda函数.P234-255

0 人点赞