C++修炼:C++11(三)

Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客: <但凡.

我的专栏: 《编程之路》《数据结构与算法之美》《题海拾贝》《C++修炼之路》

欢迎点赞,关注!

在C++11及后续版本中,Lambda表达式作为一种革命性的特性被引入,极大地改变了我们编写函数对象的方式。 Lambda表达式不仅使代码更加简洁易读 ,还为STL算法多线程编程等场景提供了极大的便利。本文将全面剖析C++中Lambda表达式的语法、特性、应用场景及最佳实践。

目录

[1、 lambda](#1、 lambda)

1.1、lambda表达式

1.2、捕捉列表

1.3、lambda表达式的实际应用

1.4、lambda的原理

2、包装器

2.1、function

2.2、bind

2.2.1、使用bind调整参数顺序

2.2.2、使用bind调整参数个数


1、 lambda

1.1、lambda表达式

**lambda本质上是一个匿名函数对象。它可以直接定义在函数内部。我们可以简单的理解为更灵活的函数。**我们可以快速简洁的定义和使用。以下是lambda表达式的规范格式:

[capture-list] (parameters) -> return type { function boby }

其中, [capture-list]是捕捉列表 ,这个列表出现在lambda函数 的开始位置。编译器根据[]来判断接下来的代码是否为lambda函数。捕捉列表可以传值或传引用捕捉。捕捉列表可以为空不能省略。

(parameters)是参数列表,跟普通函数的参数列表一样。我们要是不需要传参可以省略不写()。

-> return type是返回类型。当没有返回值时可以省略。返回值特定时也可以省略,由编译器推导函数返回值类型。

{ function boby } 就是函数体,和普通函数一样。

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
	auto add = [](int x, int y)->int {return x + y;};
	cout << add(1, 2) << endl;//3

	int a = 10;
	auto add1 = [a](int x, int y)->int {return x + y + a;};//传值捕捉
	cout << add1(1, 2) << endl;//13

	auto add2 = [&a](int x, int y)->int {return x + y + a;};//传引用捕捉
	cout << add2(1, 2) << endl;//13
	return 0;
}

1.2、捕捉列表

如果我们lambda表达式想用函数体外的变量就需要自行捕捉。当然如果你想在函数体内用的对象是全局的就不用捕捉了。

接下来我们介绍三种捕捉方式,首先是第一种,显示的传值或传引用捕捉

在1.1的代码我们已经展示过传值捕捉和传引用捕捉了。如果想捕捉多个变量需要用逗号隔开。

cpp 复制代码
	auto add2 = [a,b,&c](int x, int y)->int {return x + y + a+b+c;};//a,b传值捕捉,c传引用捕捉

第二种捕捉方式是在捕捉列表隐式捕捉。这种捕捉方式需要我们在[]中写一个=或者&,然后函数体中用什么我们自动捕捉什么变量:

cpp 复制代码
	auto add2 = [=](int x, int y)->int {return a+b+c;};//隐式值捕捉
	auto add3 = [&](int x, int y)->int {return a + b + c;};//隐式引用捕捉

第三种捕捉方式是混合使用隐式捕捉和显示捕捉。我们[]中的第一个元素必须是=或者&,表示是值捕捉或者引用捕捉,接下来的每个参数直接传变量名,他们的捕捉方式和第一个元素传的捕捉方式相反:

cpp 复制代码
	auto add2 = [=,&b,&c](int x, int y)->int {return a+b+c;};//a是传值捕捉,b,c是传引用捕捉
	auto add3 = [&,b,c](int x, int y)->int {return a + b + c;};//a是传引用捕捉,b,c是传值捕捉

捕捉列表中不能捕捉全局变量,如果lambda表达式定义在全局中,捕捉列表必须为空。

labmda捕捉列表是被const修饰的。也就是说传值捕捉的对象也不能在函数体中进行修改。但是我们使用mutable之后可以修改。

cpp 复制代码
auto add2 = [=,&b,&c](int x, int y)mutable->int {
	a+=10;
	b += 10;
	c += 10;
	return a+b+c;
	};
cout << add2(1, 2) << endl;
cout << a << " " << b << " " << c << " " << endl;//10 21 23

但需要注意的是,对于mutable修饰之后的传值捕捉,我们修改的只是拷贝过来的值,对于原来的变量没有影响。

1.3、lambda表达式的实际应用

lambda表达式可以代替仿函数完成一些工作,这个是他很大的一个贡献。因为在lambda之前我们要想写仿函数需要写一个类出来。我们如果不想用仿函数的话用函数指针定义起来也很麻烦。所以说labmda出现之后我们使用的可调用对象定义起来还是很方便的。

cpp 复制代码
int a[10] = { 1,5,6,4,8,9,1,5,13,12 };
int main()
{
	sort(a, a + 10, [](int x, int y) {return x < y;});
	for (int i = 0;i < 10;i++)
	{
		cout << a[i] << " ";
	}
	return 0;
}

当然除此以外lambda表达式还在其他地方有很多的应用,比如线程初始化,智能指针定制删除器(下下篇会说),资源管理等等。

1.4、lambda的原理

lambda底层其实就是个仿函数对象。我们写一个lambda,编译器底层就会生成一个仿函数的类。对于这个仿函数的类,编译器有几个特定的规则或者说要求:

  1. 编译器为每个Lambda生成唯一的类类型

  2. 该类包含一个重载的operator()

  3. Lambda体成为operator()的实现

  4. 捕获的变量成为该类的成员变量

**那么编译器如何确定lambda生成唯一的类类型呢?在vs系列编译器中时通过uuid来实现的。在编译器底层会和每个类名绑定一个uuid,这个uuid时随机的并且重复概率特别特别低,这就保证了每个lambda表达式都是不同的。**其他的编译器有其他的实现方式。但是确定的是需要保证lambda表达式的唯一性。

cpp 复制代码
auto test1 = [](int x, int y) {return x < y;};
auto test2 = [](int x, int y) {return x < y;};

因为唯一性的确定,对于test1和test2来说虽然他们两个功能相同,但是他们两个的类型时不同的。

2、包装器

2.1、function

function是一个类模板,也是一个包装器。我们可以使用function包装可调用对象,包括函数指针,仿函数,lambda,bind等。如果function为空,此时调用function中的目标会抛出异常。

function底层其实也是个仿函数。因为他也重载了operator()。function包含在头文件functional中。

cpp 复制代码
#include<iostream>
#include<algorithm>
#include <functional>
using namespace std;
int add(int a, int b) {
	return a + b;
}
struct Multiply {
	int operator()(int a, int b) const {
		return a * b;
	}
};
class Math{
public:
	int divide(int a, int b) {
		return a / b;
	}
};
int main()
{
	//     返回值  参数类型
	function<int(int, int)> func = add;//存储普通对象
	cout << func(2, 3);  // 输出 5

	function<int(int, int)> func1 = Multiply();//存储仿函数
	cout << func1(2, 3);  // 输出 6

	function<int(int, int)> func2 = [](int a, int b) {return a - b;};//存储lambda表达式
	cout << func2(5, 3);  // 输出 2

	Math math;
    function<int(Math*, int, int)> func3 = &Math::divide;//存储成员函数
    cout << func3(&math,6, 3);  // 输出 2
	return 0;
}

对于成员函数的包装,我们必须指定类域,并且类域前必须加取地址。而且注意我们在function的尖括号中的参数类型中必须加上隐含的this指针。 并且在调用的时候也要注意传过去一个this指针类型的对象。

但是对于成员函数,在实践中我们更喜欢这样写:

cpp 复制代码
function<int(Math&&, int, int)> func4 = &Math::divide;//存储成员函数
cout << func4(Math(), 6, 3);  // 输出 2

大家可以这样理解,我们最终的目的是调用到成员函数,那么我们可以通过this指针加->去调用成员函数,如果我们传的就是类对象呢,我们也可以通过.*这个操作符来调用到成员函数指针。所以说其实并不是一定传Math*类型的。那么这样的话我们直接传一个临时对象就可以了,就不用特定实例化出来一个对象了。

我们可以使用function和map进行结合,可以让我们的代码看起来更简洁:

150. 逆波兰表达式求值 - 力扣(LeetCode)

传统写法:

cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        for(int i=0;i<tokens.size();i++)
        {
            if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/")
            {
                int x=st.top();st.pop();
                int y=st.top();st.pop();
                switch(tokens[i][0])
                {
                    //一定要注意x和y的顺序
                    case '+':
                        st.push(y+x);
                        break;
                    case '-':
                        st.push(y-x);
                        break;
                    case '*':
                        st.push(y*x);
                        break;
                    case '/':
                        st.push(y/x);
                        break;   
                }
            }
            else
            {
                st.push(stoi(tokens[i]));//要用stoi不然无法处理多位数
            }
        }
        return st.top();
    }
};

高效写法:

cpp 复制代码
class Solution {
public:
        int evalRPN(vector<string>& tokens) {
        stack<int> st;
        // function作为map的映射可调⽤对象的类型 
        map<string, function<int(int, int)>> opFuncMap = {
            {"+", [](int x, int y){return x + y;}},
            {"-", [](int x, int y){return x - y;}},
            {"*", [](int x, int y){return x * y;}},
            {"/", [](int x, int y){return x / y;}}
        };
        for(auto& str : tokens)
        {
            if(opFuncMap.count(str)) // 操作符 
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();
 
                int ret = opFuncMap[str](left, right);
                st.push(ret);
            }
            else
            {
                st.push(stoi(str));
            }
        }
        return st.top();
 }
};

2.2、bind

bind是一个函数模板,他也是一个可调用对象的包装器。 我们可以使用它来调整参数个数和参数顺序。当然我们更常用他来调整参数个数。bind也包含在头文件functional中。

2.2.1、使用bind调整参数顺序

cpp 复制代码
#include<iostream>
#include<algorithm>
#include <functional>

using namespace std;

//展开命名空间
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int test(int a, int b) {
	return a-b;
}
int main()
{
	auto test1 = bind(test, _1, _2);
	cout << test1(10, 5) << endl;//5

	auto test2 = bind(test, _2, _1);
	cout << test2(10, 5) << endl;//-5
	return 0;
}

std::placeholders是C++标准库中与std::bind配合使用的一个命名空间,它提供了一系列占位符(_1, _2, _3等),用于表示绑定函数中将来会被提供的参数位置。我们展开他时可以像上面代码那样展开,也可以直接using namespace placeholders。

其中我们用_1,_2,_3来表示这是传参时的第几个参数,具体的对应关系可以看下图:

2.2.2、使用bind调整参数个数

我们可以使用bind绑死参数,在传参时我们只传没有被绑定的位置:

cpp 复制代码
auto test3 = bind(test, _1, 10);//绑死第二个参数
cout << test3(10) << endl;//0
cout << test3(10, 1,0,2,4,6,5,1) << endl;//0,第二个参数已经绑死,不管传不传都是10,而且参数可以传任意多个

auto test4 = bind(test,10,_1);
cout << test4(5) << endl;//5

对于test4,具体对应关系如下:

注意一点,我们传两个参数,那么绑定时就写_1,_2,不是说如果原函数第二个参数绑死了我们就传_1,_3。

接下来我们看一下bind和function的结合应用。在上面我们封装成员函数是这样写的:

cpp 复制代码
function<int(Math&&, int, int)> func4 = &Math::divide;
cout << func4(Math(), 6, 3);  // 输出 2

那么其实我们可以使用bind绑死第一个参数,就不用每次都新建一个临时对象了:

cpp 复制代码
	function<int(int, int)> func4 = bind(&Math::divide, Math(), _1, _2);
	cout << func4(6, 3) << endl;

当然我们这也可以直接用auto:

cpp 复制代码
	auto func4 = bind(&Math::divide, Math(), _1, _2);
	cout << func4(6, 3) << endl;

bind也可以绑死lambda表达式

cpp 复制代码
	auto lambda = [](int x, int y) { return x * y; };
	auto f = std::bind(lambda, _1, 5);
	std::cout << f(10) << std::endl;  // 输出50

bind也支持嵌套绑定

cpp 复制代码
int add(int a, int b) {
	return a + b;
}
int main()
{
	auto f1 = std::bind(add, std::placeholders::_1, std::placeholders::_2);
	auto f2 = std::bind(f1, 10, std::placeholders::_1);
	cout<<f2(20);  // 输出30
	return 0;
}

好了,今天的内容就分享到这,我们下期再见!