2.lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
各部分说明
- lambda表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量提供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
如下代码所示
int main() { int a = 0; int b = 2; auto add1 = [](int x, int y) ->int {return x + y; }; auto add2 = [](int x, int y) {return x + y; }; cout << add1(a, b) << endl; cout << add2(a, b) << endl; return 0; }
除此以外还可以写多行语句等等
但是要注意的是,我们如果直接去调用其他的局部的lambda表达式的话,会报错的
但是如果是一个全局的,是可以的
那么有没有办法可以使用局部的呢?其实是有的,那就是捕捉列表,比如下面的代码
那么捕捉列表有哪些捕捉方式呢?
- 捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var,捕捉后为const类型
[=]:表示值传递方式捕获所有父作用域中的变量(包括this),捕捉后为const类型
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意事项:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同
首先需要特别注意的是捕捉列表出来的是不可以修改的,如下代码所示
如果真的想修改捕捉到的变量,可以加上mutable
不过这里的mutable仅仅只是让这个变量可以被修改了。但是这里是传值的,里面的修改并不会影响外面的。实际上这个用处不大
如果想修改外面的,可以使用引用捕捉
我们还可以试一下下面的代码
int main() { int a = 0; int b = 1; int c = 2; int d = 3; int e = 4; cout << a << " " << b << " " << c << " " << d << " " << e << endl; auto func = [&] { a++; b++; c++; d++; e++; }; func(); cout << a << " " << b << " " << c << " " << d << " " << e << endl; return 0; }
除此之外,还可以混合着来,下面代码是错的,因为a不可以被修改,意思是除了a以外所有的变量使用引用捕捉,而a用值传递的方式捕捉。而a值捕捉以后是不可被修改的,所以错误
而且引用捕捉是可以捕捉const变量的。只不过捕捉以后无法修改,但是可以进行访问,他们的地址都是一样的
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
3.函数对象与lambda表达式
当我们写出这样的代码的时候,我们会发现报错了
于是我们打印出他们类型来观察一下
我们会发现其实这两个对象的类型其实是不一样的。所以当然无法赋值。
其实lambda表达式的底层就是仿函数,这里的f1,f2都是一些仿函数对象,只不过他们的类型是编译器自己生成的。我们看不到而已。
我们这里通过f1去调用的其实都是仿函数的调用
我们可以用如下代码来进行观察
class Rate { public: Rate(double rate) : _rate(rate) {} double operator()(double money, int year) { return money * _rate * year; } private: double _rate; }; int main() { // 函数对象 double rate = 0.49; Rate r1(rate); r1(10000, 2); // lamber auto r2 = [=](double monty, int year)->double {return monty * rate * year; }; r2(10000, 2); return 0; }
下面是对于仿函数的,可以看到调用了构造函数和operator()
下面是对于lambda表达式的,我们也可以看到调用了构造函数和operator()
所以lambda表达式底层其实就是仿函数,就像范围for的底层是迭代器一样
八、可变参数模板
1.可变参数模板
我们知道,printf这个函数就是一个可变参数的
这里的三个点就代表了,可以写任意个参数
这里面其实就相当于有一个数组把这个实参存起来,然后printf会依次访问数组里面的元素。
以上就是函数的可变参数
而模板参数和函数参数是很类似的,模板参数传递的是类型,函数参数传递的是对象。函数的可变参数是传多个对象,而模板的可变参数就是可以传多个类型
Args是一个模板参数包,args是一个函数形参参数包
声明一个参数包Args…args,这个参数包中可以包含0到任意个模板参数。
template<class ...Args> void Showlist(Args... args) {}
如下代码所示可以计算出有多少个可变参数
template<class T, class ...Args> void Showlist(T value, Args... args) { cout << sizeof...(args) << endl; } int main() { Showlist(1); Showlist(1, 1.1); Showlist(1, 1.1, 1.2); Showlist(1, 1.1, 1.3, 1.2); return 0; }
我们还需要注意的是,如果我们想要访问参数包的话
不可以想当然的以为这样可以取出参数包的内容,这样是错的代码,编译不通过。
我们需要这样访问
template<class T> void Showlist(T value) { cout << value << endl; } template<class T, class ...Args> void Showlist(T value, Args... args) { cout << value << " "; Showlist(args...); } int main() { Showlist(1); Showlist(1, 1.1); Showlist(1, 1.1, 1.2); Showlist(1, 1.1, 1.3, 1.2); return 0; }
它这里其实就用了一个编译时的递归。
一开始会将第一个参数传给T,然后剩下的参数包都传给下一层函数。最上面就是结束条件。
在库里面就有一个类似的接口
不过它的参数只有一个参数包,那么应该如何传递呢?其实我们可以使用一个子函数
void _Showlist() { cout << endl; } template<class T, class ...Args> void _Showlist(T value, Args... args) { cout << value << " "; _Showlist(args...); } template<class ...Args> void Showlist(Args... args) { _Showlist(args...); } int main() { Showlist(1); Showlist(1, 1.1); Showlist(1, 1.1, 1.2); Showlist(1, 1.1, 1.3, 1.2); return 0; }
其实像上面的几个函数组合起来,就相当于一个C++版本的print了,可以自动打印
关于这个打印,其实还可以这样玩
这里的核心逻辑就是,在Showlist中,会将参数包传给PrintArg这个函数,这个函数只会解析第一个参数,后面的逗号是一个逗号表达式,用于初始化数组,后面的三个点就是有几个参数就会相当于调用了几次PrintArg这个函数
void Showlist() { cout << endl; } template<class T> void PrintArg(T t) { cout << t << " "; } template<class ...Args> void Showlist(Args... args) { int a[] = { (PrintArg(args),0)... }; cout << endl; } int main() { Showlist(1); Showlist(1, 1.1); Showlist(1, 1.1, 1.2); Showlist(1, 1.1, 1.3, 1.2, string("xxxxx")); return 0; }
运行结果是
不过这段代码其实还可以稍微简化一下
不过上面的方法是一次一次取出来的,能不能一次性全部取出来呢?方便我们进行初始化等操作
如下代码所示,这样的话,我们就可以通过Create函数去调用各种情况的构造函数了,还有拷贝构造函数也可以去调用。特别灵活
class Date { public: Date(int year = 0, int month = 0, int day = 0) :_year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; }; template<class ...Args> Date* Create(Args... args) { Date* ret = new Date(args...); return ret; } int main() { Date* p1 = Create(); Date* p2 = Create(2023); Date* p3 = Create(2023, 10); Date* p4 = Create(2023, 10, 22); Date d(2023, 10, 1); Date* p5 = Create(d); return 0; }
2.emplace系列
如下接口所示,在C++11以后,很多库里面都加上了emplace系列接口
我们先看以下代码
int main() { std::list< std::pair<int, char> > mylist; mylist.push_back(make_pair(40, 'd')); mylist.push_back({ 50, 'e' }); for (auto e : mylist) cout << e.first << ":" << e.second << endl; return 0; }
这些代码都是我们之前的正常的尾插
现在有了emplace以后,我们就可以下面的写法了。
这是因为与前面的Date的实现是一样的,通过可变参数模板实现的。
int main() { std::list< std::pair<int, char> > mylist; // emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象 // 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别 mylist.emplace_back(10, 'a'); mylist.emplace_back(20, 'b'); mylist.emplace_back(make_pair(30, 'c')); mylist.push_back(make_pair(40, 'd')); mylist.push_back({ 50, 'e' }); for (auto e : mylist) cout << e.first << ":" << e.second << endl; return 0; }
也许我们也会听说emplace_back的效率更高一些,但是在上面的场景是看不出来的,下面的场景可以感受出来
int main() { // 下面我们试一下带有拷贝构造和移动构造的Sim::string,再试试呢 // 我们会发现其实差别也不到,emplace_back是直接构造了,push_back // 是先构造,再移动构造,其实也还好。 std::list< std::pair<int, Sim::string> > mylist; mylist.emplace_back(10, "sort"); mylist.emplace_back(make_pair(20, "sort")); mylist.push_back(make_pair(30, "sort")); mylist.push_back({ 40, "sort" }); return 0; }
虽然emplace效率稍高一些,但是其实还好,因为并没有太大差距,因为移动拷贝的效率很低
要是真要说的,反倒是内置类型和浅拷贝的效率可以提高一些,因为深拷贝的量级基本在一个量级。而浅拷贝是就显得差距比较大了
class Date { public: Date(int year = 0, int month = 0, int day = 0) :_year(year) , _month(month) , _day(day) { cout << "Date(int year = 0, int month = 0, int day = 0)" << endl; } Date(const Date& d) :_year(d._year) , _month(d._month) , _day(d._day) { cout << "Date(const Date& d)" << endl; } private: int _year; int _month; int _day; }; int main() { std::list< Date > mylist; mylist.push_back(Date(2023, 10, 23)); cout << endl; mylist.emplace_back(2023, 10, 23); return 0; }
其中最为核心的原因就是emplace可以传参数包,这就导致了它可以传对象,可以传对象过去。而push_back只能传对象
九、新的类功能
1.新增的默认成员函数
默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
而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: Sim::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); Person s4; s4 = std::move(s2); return 0; }
这是利用了编译器自己生成的移动拷贝
当这些类都写了的时候,调用深拷贝
如果屏蔽三个中的一个,依然是深拷贝
事实上,一般而言,我们只需判断拷贝构造、赋值重载、析构中的任意一个就可以了。因为他们三个如果要实现一般都是一起实现的,共存亡的。因为一旦写析构了必然涉及到资源的释放,涉及到了资源就必然涉及到了深拷贝。所以我们一般只要其中的一个没写那么三个基本上都不会写的。
2.一些新的关键字
- **类成员变量初始化:**C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化。
- 强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成
比如如下的例子
- 禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
- 继承和多态中的final与override关键字:final用于防止类被继承,不能被重写。override用于必须重写该虚函数
十、包装类
1.function包装器
lambda表达式很好用,但是它也有一些缺陷,那就是他的类型我们不知道,所以导致传参的时候非常难弄
面对C++中各种各样的类型,比如函数指针,仿函数,lambda表达式,有没有什么办法可以将他们统一起来呢?
我们先看下面的代码
template<class F, class T> T useF(F f, T x) { static int count = 0; cout << "count:" << ++count << endl; cout << "count:" << &count << endl; return f(x); } double f(double i) { return i / 2; } struct Functor { double operator()(double d) { return d / 3; } }; int main() { // 函数名 cout << useF(f, 11.11) << endl; // 函数对象 cout << useF(Functor(), 11.11) << endl; // lamber表达式 cout << useF([](double d)->double { return d / 4; }, 11.11) << endl; return 0; }
从运行结果上来看,这个模板被实例化成了三份
可见他们的类型各不相同,我们能否找一种办法使得只实例化成一份呢?
也就是说,将可调用对象存储到一个容器中
std::function在头文件<functional> // 类模板原型如下 template <class T> function; // undefined template <class Ret, class... Args> class function<Ret(Args...)>; 模板参数说明: Ret : 被调用函数的返回类型 Args…:被调用函数的形参
所以我们可以改善前面的代码
template<class F, class T> T useF(F f, T x) { static int count = 0; cout << "count:" << ++count << endl; cout << "count:" << &count << endl; return f(x); } double f(double i) { return i / 2; } struct Functor { double operator()(double d) { return d / 3; } }; int main() { // 函数名 cout << useF(f, 11.11) << endl; // 函数对象 cout << useF(Functor(), 11.11) << endl; // lamber表达式 cout << useF([](double d)->double { return d / 4; }, 11.11) << endl; function<double(double)> f1 = f; function<double(double)> f2 = Functor(); function<double(double)> f3 = [](double d)->double { return d / 4; }; vector<function<double(double)>> v = { f1,f2,f3 }; double n = 3.3; for (auto f : v) { cout << f(n++) << endl; } return 0; }
所以这里就完美的解决了可调用对象的类型问题
我们可以将包装器用于下面题目的改造
class Solution { public: int evalRPN(vector<string>& tokens) { stack<int> st; map<string,function<int(int,int)>> cmdFuncMap = { {"+",[](int left, int right){return left + right;}}, {"-",[](int left, int right){return left - right;}}, {"*",[](int left, int right){return left * right;}}, {"/",[](int left, int right){return left / right;}} }; for(auto& str : tokens) { if(cmdFuncMap.count(str)) { int right = st.top(); st.pop(); int left = st.top(); st.pop(); st.push(cmdFuncMap[str](left,right)); } else { st.push(stoi(str)); } } return st.top(); } };
而且这样改造之后,如果要添加运算,只需要去往map里面加数据即可,不需要做出其他改动
还是对于前面的代码,有了包装器,我们就可以将类只实例化出一份,因为可以统一成一个类型了。
template<class F, class T> T useF(F f, T x) { static int count = 0; cout << "count:" << ++count << endl; cout << "count:" << &count << endl; return f(x); } double f(double i) { return i / 2; } struct Functor { double operator()(double d) { return d / 3; } }; int main() { function<double(double)> f1 = f; function<double(double)> f2 = Functor(); function<double(double)> f3 = [](double d)->double { return d / 4; }; cout << useF(f1, 11.11) << endl; // 函数对象 cout << useF(f2, 11.11) << endl; // lamber表达式 cout << useF(f3, 11.11) << endl; return 0; }
2.bind
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
// 原型如下: template <class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args); // with return type (2) template <class Ret, class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args);
可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
调用bind的一般形式:auto newCallable = bind(callable,arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推
下面就是一个使用bind的例子
int Sub(int x, int y) { return x - y; } int main() { function<int(int, int)> rSub1 = bind(Sub, placeholders::_1, placeholders::_2); cout << rSub1(10, 5) << endl; function<int(int, int)> rSub2 = bind(Sub, placeholders::_2, placeholders::_1); cout << rSub2(10, 5) << endl; return 0; }
运行结果为
我们来详解分解一下这段代码
其实placeholders是一个命名空间,里面有很多的变量,我们先不用仔细考虑。会用就可以了
在rSub这一层第几个参数传递给下标是几的参数,但是在由进一步传入Sub的时候,是按照顺序传递的,所以导致了传递顺序的不同,从而导致了结果的不同。
所以说,这个bind的价值就是交换传递的参数顺序。
因为有一些函数的接口我们需要调整一下顺序,这时候bind就起到了很大的作用了。
而且当我们对于3个参数的函数,如果我们只想要传递两个参数,那么我们也可以用bind
double Sub(int x, int y, double rate) { return (x - y) * rate; } int main() { function<double(int, int)> rSub1 = bind(Sub, placeholders::_1, placeholders::_2, 4.2); function<double(int, int)> rSub2 = bind(Sub, placeholders::_1, placeholders::_2, 4.3); function<double(int, int)> rSub3 = bind(Sub, placeholders::_1, placeholders::_2, 4.4); cout << rSub1(10, 5) << endl; cout << rSub2(10, 5) << endl; cout << rSub3(10, 5) << endl; return 0; }
如果我们想要将固定的参数给到前面,那就是这样的,注意一定是从_1开始的下标
double Sub(int x, int y, double rate) { return (x - y) * rate; } double RSub(double rate, int x, int y) { return (x - y) * rate; } int main() { function<double(int, int)> rSub1 = bind(Sub, placeholders::_1, placeholders::_2, 4.2); function<double(int, int)> rSub2 = bind(Sub, placeholders::_1, placeholders::_2, 4.3); function<double(int, int)> rSub3 = bind(Sub, placeholders::_1, placeholders::_2, 4.4); cout << rSub1(10, 5) << endl; cout << rSub2(10, 5) << endl; cout << rSub3(10, 5) << endl; function<double(int, int)> rSub4 = bind(RSub, 4.5, placeholders::_1, placeholders::_2); function<double(int, int)> rSub5 = bind(RSub, 4.2, placeholders::_1, placeholders::_2); cout << rSub4(10, 5) << endl; cout << rSub5(10, 5) << endl; return 0; }
因为这个_1和_2其实是给rsub看的。只有他们才会去看这些下标,后面的都是直接传递的
如下所示的场景中,
我们需要注意的是,如果某个函数是某个类域里面的,我们还要记得写上访问限定符,因为它只能访问到这个局部域和全局域中的。
对于静态的函数,写上类域就可以了,但是对于非静态的,它的地址还需要加上取地址符号,静态的可以加也可以不加,除此之外,还需要传递一个this指针,为此我们需要定义一个对象,才能传过去,或者直接传递一个对象也是可以的
class SubType { public: static int Sub(int x, int y) { return (x - y); } int SSub(int x, int y, double rate) { return (x - y) * rate; } }; int main() { function<int(int, int)> rSub5 = bind(SubType::Sub, placeholders::_1, placeholders::_2); SubType sb; function<int(int, int)> rSub6 = bind(&SubType::SSub, &sb, placeholders::_1, placeholders::_2, 3); function<int(int, int)> rSub7 = bind(&SubType::SSub, SubType(), placeholders::_1, placeholders::_2, 3); cout << rSub5(10, 5) << endl; cout << rSub6(10, 5) << endl; cout << rSub7(10, 5) << endl; return 0; }
对于类域中的非静态,传地址我们可以理解,但是为什么可以传对象呢?
其实bind的底层其实也是仿函数,在这个变量这里重载了operator(),可以根据传入的是对象还是指针去决定最终传递哪一个。所以可以可以传一个对象过去
不过要切记,不可以给匿名对象取地址,因为右值无法取地址