【C++11】lambda表达式 && 可变参数模板 && 移动构造与移动赋值 && 包装器

发布于:2025-04-08 ⋅ 阅读:(21) ⋅ 点赞:(0)

目录

一、lambda表达式:

lambda表达式语法:

lambda表达式的使用:

lambda表达式的底层原理:

二、可变参数模板:

参数包:

递归解析参数包:

逗号表达式解析参数包:

STL中的emplace:

三、移动构造和移动赋值:

四、包装器:

function:

bind:


一、lambda表达式:

如下是一个描述水果的结构体,其中有着水果名,价格,数量

struct Fruit
{
	string _name;
	int _price;
	int _num;
};

接着在主函数中,创建一个vector来存放水果并初始化

vector<Fruit> v = { {"苹果",5,10},{"梨子",4,9},{"西瓜",10,2},{"橙子",6,5} };

接着想要对上述的vector进行排序,分别按照价格的降序,升序,数量的降序,升序

用简单的sort函数是不行的,在以前,我们会使用仿函数来进行判断操作:

如下就是搞四个仿函数:

struct PriceLess
{
	bool operator()(const Fruit& f1,const Fruit& f2)
	{
		return f1._price > f2._price;
	}
};

struct PriceGreater
{
	bool operator()(const Fruit& f1, const Fruit& f2)
	{
		return f1._price < f2._price;
	}
};

struct NumLess
{
	bool operator()(const Fruit& f1, const Fruit& f2)
	{
		return f1._num > f2._num;
	}
};

struct NumGreater
{
	bool operator()(const Fruit& f1, const Fruit& f2)
	{
		return f1._num < f2._num;
	}
};

接着在sort的最后加上一个仿函数的类,这样就能够完成四种排序:

	sort(v.begin(), v.end(), PriceLess());//按照价格降序
	sort(v.begin(), v.end(), PriceGreater());//按照价格升序序
	sort(v.begin(), v.end(), NumLess());//按照数量降序
	sort(v.begin(), v.end(), NumGreater());//按照数量升序序

这样,在main函数中,就能够成功打印出排好序的水果了

但是如果这样写当定义仿函数的时候,不按照规范定义函数,并且定义的仿函数离调用的地方远得很,这样就会降低代码的可读性

在C++11里面引入了lambda表达式,在这种场景就可以使用lambda表达式来增强代码的可读性

lambda表达式是一个匿名函数,恰当使用lambda表达式可以让代码变得简洁,并且可以提高代码的可读性

lambda表达式语法:

lambda表达式完整格式:

[capture-list] (parameters) mutable -> return-type {statement};

[capture-list]:这是捕捉列表,在lambda表达式的第一个位置,是不可以省略的,如果不捕捉就写[],其作用是捕捉上文中的变量提供给lambda函数使用,但是捕捉的变量是不能够修改的,如果想要修改就需要加上后面讲的mutable

[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针

注意:

1、上述的父作用域是指包含含lambda函数的语句块

2、语法上捕捉列表可由多个捕捉项组成,并以逗号分割

        比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
                   [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

3、捕捉列表不允许变量重复传递,否则就会导致编译错误。
        比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

4、在块作用域以外的lambda函数捕捉列表必须为空

5、在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者
非局部变量都会导致编译报错

6、lambda表达式之间不能相互赋值,即使看起来类型相同

(parameters):参数列表,和普通的函数的参数列表是一样的,如果没有参数要传参的话,是可以省略的

mutabl:这个一般是省略的,在默认情况下,lambda函数是一个const函数,mutable可以取消其常量性,当使用该修饰符时,参数列表不可省略(即使参数为空)

-> return-type:这是返回值类型,一般可以省略,如果没有返回值省略,有明确返回值的时候,会让编译器自动推导,所以也可以省略

{statement}:这是函数主体,可以使用在参数列表中传进来的参数,也可以使用捕捉列表中捕捉的参数

lambda表达式的使用:

下面通过lambda表达式构建一个简单的两整数相加

本来是一个匿名函数,可以交给一个有名函数对象,这样使用起来就和正常的函数没什么两样了

int main()
{
	int x = 1, y = 2;
	//auto自动推导类型
	auto retfun = [](int& a, int& b) { return a + b; };
	int ret = retfun(x, y);//使用时类似于函数
	cout << ret << endl;
	return 0;
}

也可以只写一行,这也就是匿名函数

int main()
{
	int x = 1, y = 2;
	auto ret = [](int& a, int& b) { return a + b; }(x, y);
	cout << ret << endl;
	return 0;
}

当然,在函数体里面也不只能写一行,也可以写多行,接下来实现一个swap:

int main()
{
	auto Swap = [](int& x, int& y) {
		int tmp = x;
		x = y;
		y = tmp;
		};
	int a = 1, b = 2;
	cout << "a = " << a << " b = " << b << endl;
	Swap(a, b);
	cout << "a = " << a << " b = " << b << endl;
	return 0;
}

这里也可以使用一下引用捕捉列表:

int main()
{
	int a = 1, b = 2;
	cout << "a = " << a << " b = " << b << endl;
	auto Swap = [&]() {
		int tmp = a;
		a = b;
		b = tmp;
		};
	Swap();
	cout << "a = " << a << " b = " << b << endl;
	return 0;
}

接着使用一下引用捕捉结合值捕捉:

int main()
{
	int a = 1, b = 1;
	cout << "a = " << a << " b = " << b << endl;
	auto Swap = [&a,b]()mutable {
		a++;
		b++;
		};
	Swap();
	cout << "a = " << a << " b = " << b << endl;
	return 0;
}

如上,在函数调用前后,发现a被++了,但是b没有被++,这其实和函数那里的传引用和传值是差不多的,这里的a是一样的,里面的b其实是外面的b的一份拷贝,所以自然也就不会影响外面的b

lambda表达式的底层原理:

实际上,在编译器中,lambda表达式的底层是被转化成了仿函数4

首先:我们看看函数,仿函数,lambda表达式生成的函数对象

int add(int x, int y)
{
	return x + y;
}

class Add
{
public:
	int operator()(int x,int y)
	{
		return x + y;
	}
};

int main()
{
	//普通函数
	int x = 1,y = 2;
	auto add1 = add(1,2);

	//仿函数函数
	Add add2;

	//lambda表达式
	auto add3 = [](int x,int y) { return  x + y; };
	add3(x,y);

	cout << "函数 : " << sizeof(add1) << endl;
	cout << "仿函数 : " << sizeof(add2) << endl;
	cout << "lambda表达式 : " << sizeof(add3) << endl;

	return 0;
}

所以可以发现,仿函数和lambda表达式是一样的,其中仿函数是一个空类,其没有成员变量,大小只为1字节,所以lambda表达式也是生成了一个空类

接下来看看仿函数和lambda表达式的反汇编:

class Add
{
public:
	Add(int base)
		:_base(base)
	{}
	int operator()(int num)
	{
		return _base + num;
	}
private:
	int _base;
};
int main()
{
	int base = 1;

	//函数对象
	Add add1(base);
	add1(2);

	//lambda表达式
	auto add2 = [base](int num){ return base + num; };
	add2(2);
	return 0;
}

将上述代码转化为反汇编,我们可以看到:

在Add函数中,创建对象的时候调用构造函数,并且在后面仿函数中调用了operator()

接下来观察lambda表达式,同样也观察到了类似的代码

在上述的汇编代码中,也可以看到:Add函数在创建函数对象的时候,会调用构造,在使用函数对象的时候,会调用operator(),

同样的道理在lambda表达式中,编译器会先自动生成一个lambda类,在这个类中,会重载operator(),所以lambda表达式的底层实现就是仿函数

lambda表达式的类:

我们可以通过typeid(ret).name(),这样来观察lambda表达式生成的类型


int main()
{
	auto ret1 = [](int x, int y) {return x + y; };
	auto ret2 = [](int x, int y) {return x + y; };
	auto ret3 = [](int x, int y) {return x + y; };

	cout << typeid(ret1).name() << endl;
	cout << typeid(ret2).name() << endl;
	cout << typeid(ret3).name() << endl;

	return 0;
}

如上,可以看到,lambda表达式生成的类型是不同的,即使表面上看起来lambda表达式是一模一样的,但是编译器会将这些处理成不同的类名,这样就算是两个一模一样的lambda表达式,它们的类型都是不同的

二、可变参数模板:

在C++11以前,类模板和函数模板只能包含固定数量的模板参数,但是在C++11中增添了新特性,能够让用户定义任意数量的参数模板函数,这样大大提高了模板参数泛化

其实可变参数我们已经使用过了,在如下的printf中:

如上,在printf中,可以加上任意个%d,这就是可变参数列表,也就是随便传入多个参数,函数都能够进行接收

那么回到我们的可变参数模板

参数包:

把所有传入的参数,不论数量,类型,统统进行打包,形成了可变参数包

如下为可变参数模板的一般形式:

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
template <class ...Args>
void func(Args... args)
{
    //函数体
}

那么接下来传参的时候,就能够允许传任意数量,任意类型的参数,这样能够极大提高泛化

template<class ...Args>
void Func(Args... argc)
{}

int main()
{
	Func(1);
	Func(1, 2);
	Func(1, 1.1);
	Func(1, 'x');
	Func(1, "xxxxx");

	return 0;
}

并且在函数内部还能够打印传入了多少个参数:

template<class ...Args>
void Func(Args... argc)
{
	cout << sizeof...(argc) << endl;
}

int main()
{
	Func(1);                    //1
	Func(1, 2, 3, 4, 5);        //5
	Func(1, 1.1, 'x', "xxxxx"); //4
	Func(1,'x');                //2
	Func(1, "xxxxx");           //2

	return 0;
}

递归解析参数包:

但是对于这个参数包里面的类型不能够直接拿到,这也是一个缺点,如果想拿是只能够拿到第一个参数类型的,所以就需要通过函数递归来进行参数包的解析:

递归思路:

  • 我们给函数模板不传一整个参数包,而是一个函数类型模板参数+函数模板参数包
  • 这样每一次进入这个函数就能够让第一个参从参数包中分离
  • 接着解析完这个参数包后,在后面继续递归使用这个参数包,进而每次将参数包中的第一个参数解析出来,然后还要写一个无参数的函数重载,当参数包中没有参数之后,就会走无参数的重载函数,这样就能够让递归退出
int num = 1;

//这是递归出口函数,是func函数的重载
//当参数包中的参数为空后就调用这个函数结束
void Func()
{
	num = 1;
	cout << endl;
}

//每次都解析第一个参数,在后面递归Func函数,进而每次都解析函数包的第一个参数
template<class T, class ...Args>
void Func(const T& t,Args... args)
{
	cout << "第" << num++ << "个参数是 : " << t << endl;
	Func(args...);
}

int main()
{
	cout << "Func(1) : " << endl;
	Func(1);                  
	cout << "Func(1, 2, 3, 4, 5);" << endl;
	Func(1, 2, 3, 4, 5);       
	cout << "Func(1, 1.1, 'x', xxxxx); " << endl;
	Func(1, 1.1, 'x', "xxxxx"); 
	cout << "Func(1,'x');" << endl;
	Func(1,'x');               
	cout << "Func(1, xxxxx);" << endl;
	Func(1, "xxxxx");      

	return 0;
}

逗号表达式解析参数包:

本质是通过数组的列表初始化,当创建数组的时候,将参数包传入数组,然后当数组进行初始化的时候,将参数包展开了,展开后就完成了解析的工作

template <class T>
int FuncArg(const T& t)
{
	cout << "该参数类型是 : " << typeid(t).name() << " 内容是 : " << t << endl;
	return 0;
}

//args代表0-N的参数包
template <class ...Args>
void Func(Args... args)
{
	int a[] = { FuncArg(args)...};
	cout << endl;
}

int main()
{
	cout << "Func(1) : " << endl;	//在上述展开后int a1[] = { FuncArg(1) };
	Func(1);                  
	cout << "Func(1, 2, 3, 4, 5);" << endl;//在上述展开后int a2[] = { FuncArg(1),FuncArg(2),FuncArg(3),FuncArg(4),FuncArg(5) };
	Func(1, 2, 3, 4, 5);       
	cout << "Func(1, 1.1, 'x', xxxxx); " << endl;//在上述展开后int a3[] = { FuncArg(1),FuncArg(1.1),FuncArg('x'),FuncArg("xxxxx")};
	Func(1, 1.1, 'x', "xxxxx"); 
	cout << "Func(1,'x');" << endl;//在上述展开后int a4[] = { FuncArg(1),FuncArg('x')};
	Func(1,'x');               
	cout << "Func(1, xxxxx);" << endl;//在上述展开后int a5[] = { FuncArg(1),FuncArg("xxxxxx") };
	Func(1, "xxxxx");      

	return 0;
}

STL中的emplace:

在C++11中引入了emplace这样的接口,这些新增的函数依赖可变参数包

 如上参数部分并不是右值引用,而是万能引用,其既可以接收左值引用,也可以接收右值引用,还可以接收参数包

emplace系列接口的使用方式

emplace和原容器的插入接口类似,例如emplace_back具备了push_back的所有功能,并且还具备更大的优势

这里以list为例:

其中,无论是push_back还是emplace_back都既能够传左值,又能够传右值,但是push_back能够进行列表初始化,emplace_back不能够进行列表初始化

int main()
{
	list<pair<int, ppr::string>> lt1;
	cout << "emplace_back" << endl;
	cout << endl;
	cout << "传左值" << endl;
	pair<int, ppr::string> kv1(1, "one");
	lt1.emplace_back(kv1);								//传左值
	cout << endl;
	cout << "传右值" << endl;
	lt1.emplace_back(pair<int, ppr::string>(2, "two")); //传右值
	cout << endl;
	cout << "传参数包" << endl;
	lt1.emplace_back(3, "three");                      //传参数包
	cout << endl;
	cout << "push_back" << endl;
	cout << endl;
	list<pair<int, ppr::string>> lt2;
	cout << "传左值" << endl;
	pair<int, ppr::string> kv2(1, "one");
	lt2.push_back(kv2);								//传左值
	cout << endl;
	cout << "传右值" << endl;
	lt2.push_back(pair<int, ppr::string>(2, "two")); //传右值
	cout << endl;
	cout << "列表初始化" << endl;
	lt2.push_back({ 3, "three" });					  //列表初始化
	return 0;
}

通过上述的代码,我们可以知道:

无论是emplace_back还是push_back,当传左值的时候都是一样的,首先实例化左值对象,这步是调用构造函数的,接着匹配拷贝构造,进而实现深拷贝

当传右值的时候,实例化右值对象,这里就走移动构造,进而实现移动拷贝

当emplace传入参数包的时候,这直接将纯右值作为参数包进行传递,传递中不展开参数包,直到构造函数再将参数包展开,这样直接传递参数,得益于参数包的优势

emplace的意义:

emplace系列相比于push_back的特点是传入参数包,利用参数包直接构造出对象,这样就能减少一次拷贝,这就是为什么emplace系列接口更高效

emplace系列接口并不是在所有场景下都比原有的插入接口高效,如果传入的是左值对象或右值对象,那么emplace系列接口的效率其实和原有的插入接口的效率是一样的

emplace系列接口真正高效的情况是传入参数包的时候,直接通过参数包构造出对象,避免了中途的一次拷贝

三、移动构造和移动赋值:

在C++11之前,有6个默认成员函数:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 拷贝复制函数
  • 取地址重载函数
  • const取地址重载函数

这些就是如果不写的话,编译器会默认生成的

在C++11之后,有了右值引用+移动语义,所以在类中对应生成了两个新的默认构造函数:移动构造和移动赋值运算符重载

什么时候会生成移动构造函数:

  • 没有自己实现移动构造函数
  • 没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个

满足以上两点,并且没有自己写移动构造,那么编译器就会自动生成一个移动构造

默认生成的移动构造有什么用:

  • 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝
  • 自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用它自己的移动构造,没有实现就调用拷贝构造

什么时候会生成移动赋值重载函数:

  • 没有自己实现移动赋值重载函数
  • 没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个

满足以上两点,并且没有自己写移动赋值重载函数,那么编译器会自动生成一个移动赋值重载函数

默认生成的移动赋值重载函数有什么用:

  • 默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝
  • 自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用它自己的移动赋值,没有实现就调用拷贝赋值

接下来通过代码感受:

class Person
{
public:
	//构造函数
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	//拷贝构造函数
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
	//拷贝赋值函数
	Person& operator=(const Person& p)
	{
		if (this != &p)
		{
			_name = p._name;
			_age = p._age;
		}
		return *this;
	}
	//析构函数
	~Person()
	{}
private:
	ppr::string _name; 
	int _age;        
};

int main()
{
	Person s1("zhangsan", 20);
	cout << endl;
	Person s2 = std::move(s1);

	return 0;
}

如上,这是写的一个Person类,实现Person类中的string是我们自己写的string类的,一开始将析构函数 、拷贝构造、拷贝赋值重载中的任意一个或多个都不屏蔽,当运行的时候,发现会走拷贝构造

当将Person类中的拷贝构造,拷贝赋值,析构函数都屏蔽掉,再次运行程序就会发现走的是移动构造函数

四、包装器:

function:

到现在为止,我们已经学习了三种可调用函数对象类型

函数指针

仿函数

lambda表达式

所以在调用函数的时候,如:int ret = func();

那么这个func是什么呢 ----- 可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?所以这些都是可调用的类型

并且如果想让这些对象用同一个vector装起来显然是不能够直接装的,这里也不能使用auto,因为auto是推导类型,所以在C++11中,就引入了function这个包装器,将可调用对象进行包装,function是基于可变参数模板实现的

原型:

#include <functional>
template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;

Ret:被调用函数的返回值

Args:被调用函数的形参

这样就能够通过包装器来包装之前的三个函数对象了

int func1(int a,int b)
{
	cout << "void func1(int a) : "<< endl;
	return a + b;
}

struct func2
{
public:
	int operator()(int a,int b)
	{
		cout << "void operator()(int a) : "<< endl;
		return a + b;

	}
};

int main()
{
	auto func3 = [](int a,int b)->int {
		cout << "auto lambda = [](int a,int b)->int  : " << endl;
		return a+b; 
		};

	function<int(int,int)> f1 = func1;
	function<int(int,int)> f2 = func2();
	function<int(int,int)> f3 = func3;

	vector<function<int(int,int)>> v = {f1,f2,f3};

	int n = 1;
	for (auto& ch : v)
	{
		cout << ch(n,n) << endl;
		n++;
	}
	return 0;
}

这样,就能够将他们全部放入同一个vector中了

function解决模板效率低下问题,减少实例化

template<class F,class T>
double Func(F f,T t)
{
	static int count = 0;
	count++;
	cout << "count = " << count << " &count = " << &count << endl;
	return f(t);
};

double func1(double n)
{
	return n / 3;
}

struct func2
{
public:
	double operator()(double n)
	{
		return n / 4;
	}
};

int main()
{
	//function<double(double)> f1 = func1;
	//cout << Func(f1, 10) << endl;
	cout << Func(func1, 10) << endl;

	//function<double(double)> f2 = func2();
	//cout << Func(f2, 10) << endl;
	cout << Func(func2(), 10) << endl;

	//function<double(double)> f3 = [](int n)->double { return n / 5;  };
	//cout << Func(f3, 10) << endl;
	cout << Func([](double n)->double { return n / 5;  }, 10) << endl;
	return 0;
}

如上,通过上述count的值,我们知道这是因为Func这个函数被实例化了三份,这样如果很多的话就会很消耗资源的,但是也可以用function包装器将函数指针,仿函数,lambda表达式包装起来,这样就会只实例化出一份

int main()
{
	function<double(double)> f1 = func1;
	cout << Func(f1, 10) << endl;

	function<double(double)> f2 = func2();
	cout << Func(f2, 10) << endl;

	function<double(double)> f3 = [](int n)->double { return n / 5;  };
	cout << Func(f3, 10) << endl;

	return 0;
}

function包装器的意义

  • 将可调用对象的类型进行统一,便于我们对其进行统一化管理
  • 包装后明确了可调用对象的返回值和形参类型,更加方便使用者使用

bind:

这个包装器用来接收一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表,可以修改参数传递时的位置以及参数个数,C++中的bind本质是一个函数模板

函数原型:

template <class Fn, class... Args>
bind(Fn&& fn, Args&&... args);

参数解析:

fn:可调用对象

args:要绑定的参数列表,这里是万能引用不是右值引用,传值或者是占位符

占位符:placeholders作用域中的_1,_2等等

bind改变传参顺序


void Func(int a, int b)
{
	cout << "int Func(int a, int b)中 a = " << a << " b = " << b << endl;
}

int main()
{
    //正常调用
	Func(1, 2);
    //bind函数改变传参
	auto func = bind(Func, placeholders::_2, placeholders::_1);
	func(1, 2);

	return 0;
}

不仅仅可用auto推导,还可以用包装器包装类型

function<void(int,int)> func = bind(Func, placeholders::_2, placeholders::_1);

在使用 bind 绑定改变参数传递顺序时,参与交换的参数类型,至少需要支持隐式类型转换,否则是无法交换传递的

bind固定参数

bind还可以固定住一个参数,如上,我们将第一个参数固定为100,后面就只需要传一个参数即可‘’


void Func(int a, int b)
{
	cout << "int Func(int a, int b)中 a = " << a << " b = " << b << endl;
}

int main()
{
	auto func = bind(Func, 100, placeholders::_1);
  	//function<void(int)> func = bind(Func, 100, placeholders::_1);

    func(1);
	func(2,3);
	return 0;
}

如上,如果参数多了的话,后面多出来的就会被舍弃掉,并且无论传参顺序是怎么样的,placeholders作用域中总是从_1开始的

bind包装器的意义

  • 将一个函数的某些参数绑定为固定的值,让我们在调用时可以不用传递某些参数。
  • 可以对函数参数的顺序进行灵活调整。