目录
一、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包装器的意义
- 将一个函数的某些参数绑定为固定的值,让我们在调用时可以不用传递某些参数。
- 可以对函数参数的顺序进行灵活调整。