C++11-17中的lambda

本文详细介绍了C++11-17中的lambda表达式,包括可调用对象、谓词、lambda的捕捉列表、可变lambda、异常说明、指定返回类型等特性,并通过实例解析了如何使用lambda实现特定功能,如一元和二元谓词,以及如何处理捕捉列表中的值捕获和引用捕获。同时,文章提到了泛型lambda、constexpr lambda和向lambda传递this的拷贝等C++14和C++17的新特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

可调用对象

  可调用对象,有函数、函数指针、重载了函数调用运算符的类(也就是仿函数)、lambda表达式四种形式。这里主要介绍lambda的作用。

谓词

  先看下面的代码:

static bool cmp(const int& a, const int& b) { return a > b; }
int main() {
	vector<int> arr{ 1, 4, 5, 2 };
	sort(arr.begin(), arr.end(), cmp);
	for (auto w : arr) {
		cout << w << " ";
	}
}

  由于STL中的sort函数默认是less(升序排序),这里为了演示就调用了sort的重载版本,传入一个二元谓词版本。这里先解释下谓词是什么?
  谓词是一个可调用的表达式(也就是可调用对象),其返回结果是一个能用作条件的值。标准库算符所使用的谓词分为:一元谓词(只接受单一参数),和二元谓词(接受两个参数),没有三元、四元谓词。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
  这里再来看上面的cmp函数为什么要加static声明,其实在上面的例子中加不加都能通过。但是最好还是加上,因为如果在一个类中定义了cmp函数,如果在成员函数中调用这个带谓词的sort函数会调用失败,因为std::sort是一个全局函数,其传入的谓词要是全局或者是静态的。
  由于谓词只有一元和二元之分,那么有时候希望进行的操作需要更多的参数,超出了算法对谓词的限制。下面有个需求,找出vector中字符长度大于length的第一个元素。find_if是find的一元谓词版本,其需要一对迭代器和一元谓词,该函数返回第一个使谓词返回非0值得元素,如果不存在这样得元素,则返回尾迭代器。

static bool cmp(const string& s) { return s.size() < 4; }
int main() {
	vector<string> arr{ "hello", "world!", "C++", "Welcome", "you" };
	auto it = find_if(arr.begin(), arr.end(), cmp);
	cout << *it << endl;
}
// 结果显式:C++

  这里规定了比较的长度为4,如果想传入一个长度变量怎么办?但是STL规定了传递给find_if任何函数都必须严格接受一个参数,所以没有任何办法能传递给它两个参数。下面可以用lambda来实现(lambda也是传入一个参数,但是其通过其它功能来得到长度变量)

lambda

  lambda可以理解成一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但是与别的函数不同,lambda可能定义在函数内部。

[capture list](parameter list)  mutable或异常exception ->return type { function body }
  • [capture list]:捕获列表,是一个lambda所在函数中可以访问的变量的列表(通常为空)。其总是出现在lambda函数的开始处。事实上,[]是lambda引出符。编译器根据该引出符判断接下来的代码是否是lambda函数。
  • (parameter list):参数列表,与普通函数一样,但是如果不需要参数传递,则可以连同括号()一起省略。
  • mutable:修饰符,默认情况下lambda捕获的变量是按值传递的,不能被改变,如果加上mutable修饰符可以改变其变量。在使用该修饰符时,参数列表不可省略。
  • 异常说明exception:用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)。
  • ->return-type:返回类型。处于方便,不需要返回值的时候也可以连同符号->一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
  • { function body }:函数体。内容与普通函数一样,不过除了可以使用参数之外,还以可以使用所有捕获的变量。
      下面先通过lambda实现上述的find_if,然后在根据lambda形式介绍各个部分。
int main() {
	int length = 4;
	vector<string> arr{ "hello", "world!", "C++", "Welcome", "you" };
	auto lambda = [length](string& s) { return s.size() < length; };
	auto it = find_if(arr.begin(), arr.end(), lambda);
	cout << *it << endl;
}
// 结果显式:C++
捕捉列表

  其可以为空,但是不可以省略。其可以捕获当前函数中上下文数据,描述了上下文中哪些数据可以被lambda使用,以及使用方式(按值还是按引用传递)。需要注意的是,lambda仅能捕捉当前函数作用域中的自动变量,不能捕捉static变量或者全局变量,局部变量只有在捕捉列表中捕获了,才可以在lambda函数体中使用(其含义是lambda函数体中可以直接使用局部static变量和它所在函数之外声明的名字)
  语法上,捕捉列表由多个捕捉项组成,并以逗号分割。捕捉列表有如下几种形式:

  • [var] 表示值传递方式捕捉变量var。
  • [=] 表示值传递方式捕捉所有当前作用域的变量(包括this)
  • [&var] 表示引用传递捕捉变量var
  • [&] 表示引用传递捕捉所有当前作用域的变量(包括this)
  • [this] 表示值传递方式捕捉当前的this指针
  • [=, &a, &b] 表示以引用传递的方式捕捉变量a和b,值传递方式捕捉其它所有变量
  • [&, a, this] 表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其他所有变量
  • [=, a] 这里 = 已经以值传递方式捕捉了所有变量,捕捉a重复
  • [&, &this] 同理
值捕获

  与传值参数类型,采用值捕获的前提是变量可以拷贝。以参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。

int main() {
	int length = 4;
	vector<string> arr{ "hello", "world!", "C++", "Welcome", "you" };
	auto lambda = [length](string& s) { return s.size() < length; };
	length = 7;
	auto it = find_if(arr.begin(), arr.end(), lambda);
	cout << *it << endl;
}
// 仍旧输出:C++
// 由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值
引用捕获

  以引用捕获,即可以在lambda函数中进行修改该值。由于可以从函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda不能包含引用捕获(因为引用捕获的局部变量在函数退出时已经销毁了)

int main() {
	int length = 4;
	vector<string> arr{ "hello", "world!", "C++", "Welcome", "you" };
	auto lambda = [&length](string& s) { length = 10;  return s.size() < length; };
	auto it = find_if(arr.begin(), arr.end(), lambda);
	cout << length << endl;
}
// 输出:10
可变lambda:mutable

  默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果希望能改变一个被捕捉的变量的值,就必须在参数列表后加上mutable修饰符。在使用该修饰符时,参数列表不可省略。

int main() {
	int length = 4;
	vector<string> arr{ "hello", "world!", "C++", "Welcome", "you" };
	auto lambda = [length](string& s) mutable { length = 10;  return s.size() < length; };
	auto it = find_if(arr.begin(), arr.end(), lambda);
	cout << length << endl;
}
异常说明

  lambda如果没有显式声明异常说明符,其默认是throw (),即不抛出任何异常。当然也可以显式throw(int)异常。

int main() {
	auto lambda = []() throw(int) {
		throw(5);
	};
	try {
		lambda();
	}
	catch (int&) {
		cout << "捕捉到了int类型异常" << endl;
	}
}
指定lambda返回类型

  上述代码中,都没有显式指定lambda的返回类型,都由编译器自行判断。

auto lambda = [length](string& s)  { return s.size() < length; }; // 返回bool
auto lambda = []() throw(int) { throw(5); };  // 返回void

  那么什么时候需要指定lambda然会类型呢,当函数体中有两个return时,且这两个return返回值类型不同。

auto lambda = [length](string& s) { 
	if (length < 5) return s.size() < length;
	else return 1;
};  
// 编译出错,因为return 1返回int, return s.size() < length返回bool类型
// 如果都是返回bool类型,是不会报错的,这个时候可以显式指定返回类型
auto lambda = [length](string& s) ->bool { 
	if (length < 5) return s.size() < length;
	else return 1;
};
lambda 捕捉表达式

  前面讲过,lambda表达式可以按复制或者引用捕获在其作用域范围内的变量。而有时候,我们希望捕捉不在其作用域范围内的变量,而且最重要的是我们希望捕捉右值。所以C++14中引入了表达式捕捉,其允许用任何类型的表达式初始化捕捉的变量。

int main() {
	auto lambda = [str = "hello,world!"]{ return str; };
	cout << lambda() << endl;
}
// 输出hello,world

int x = 4;
int main() {
	auto lambda = [&r = x, x = x + 1]{ r += 2; return x*x; };
	cout << lambda() << endl;
    cout << x << endl;
}
// 输出:25,6
// r是x的引用,所以r变了,外面的x也变了;而捕捉列表的x = x+1 = 5,相当于值传递与外界的全局x没有关系

int main() {
	unique_ptr<int> p = make_unique<int>(5);
	auto lambda = [v = std::move(p)]{ return *v; };
	cout << lambda() << endl;
}
// 由于unique_ptr是不能拷贝的,所以只能通过移动语义来获取右值
泛型lambda

  从C++14 开始,Lambda 函数的形式参数可以使用auto关键字来产生意义上的泛型。

int main() {	
	auto lambda = [=](auto x, auto y){ return x*y; };
	cout << lambda(3,4.5) << endl;
}
constexpr lambda

  从C++17开始,如果lambda表达式符合要求的话,该表达式会隐式转换为constexpr表达式。(表达式内没有静态变量,没有虚函数,没有 try/catch语句, 没有new/delete关键字)

// gcc version 8.2.0 (MinGW.org GCC-8.2.0-5)
int main() {	
	auto lambda = [](auto x) { return x; };
	int arr[lambda(1)] = {1};
	cout << arr[0] << endl;
}

// 输出:1

  题外话,当初下载了vs2017也没怎么管啥具体版本,但是今天才发现vs2017对C++17支持并不是全部,具体可以看vs微软官方文档,查了下constexpr lambda需要vs2017.15.3版本,而我的版本是15.0。。

  上述程序中可以看到,lambda隐式转成了constexpr,如果不确定是否可以转成constexpr,可以加constexpr修饰符来判断

int main() {	
	static int x = 5;
	auto lambda = []() constexpr { static int y = 5; return x; };   // error
	int arr[lambda()] = {1};
	cout << arr[0] << endl;
}
向lambda传递this的拷贝

  如果成员函数内有lambda表达式,在表达式内希望调用其他成员函数或变量时,我们需要在[]中添加捕捉对象说明。

class Test {
private:
    string name = "hello";
public:
    void foo() {
        auto l1 = [] { cout << name << endl; }; 
        auto l2 = [=] { cout << name << endl; };
        auto l3 = [&] { cout << name << endl; };
		l1();
		l2();
		l3();
    }
};

int main() {	
	Test t;
	t.foo();
}

  然而这有一个问题,就是说如果lambda表达式在调用时,传递进来的对象已经销毁了,这样就会引发错误。

class Test {
private:
	string name = "hello";
public:
	thread startThread() const {
		thread t(
			[this] {
				cout << "I will shellp 3 seconds" << endl;
				this_thread::sleep_for(std::chrono::seconds(3));
				cout << name << endl;
			}
		);
		return t;
	}
};

int main() {	
	std::thread t;
	{
		Test d;
		t = d.startThread();
	} // d已经销毁
	cout << "the main thread wait for sub thread end." << endl;
	t.join();
}

// 由于d对象已经销毁了,所以cout << name << endl是未定义的

  C++17中,我们可以在lambda表达式的捕获类别里[]写上*this,表示传递到lambda中的是this对象的拷贝。从而解决上述的问题。

class Test {
private:
	string name = "hello";
public:
	thread startThread() const {
		thread t(
			[*this] {
				cout << "I will shellp 3 seconds" << endl;
				this_thread::sleep_for(std::chrono::seconds(3));
				cout << name << endl;
			}
		);
		return t;
	}
};

int main() {	
	std::thread t;
	{
		Test d;
		t = d.startThread();
	} // d已经销毁
	cout << "the main thread wait for sub thread end." << endl;
	t.join();
}
参考资料

(1)《C++Primer》
(2)《深入理解C++11·C++11新特性解析与应用》
(3)《快速上手C++11/14》
(4)https://blog.youkuaiyun.com/Hello_World_156_5634/article/details/90670637
(5)https://www.cnblogs.com/AlainGao/p/10675201.html
(6)https://blog.youkuaiyun.com/janeqi1987/article/details/100176985

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值