目录
一、可变参数模版二、emplace_back三、lambda表达式四、包装器4.1 function4.2 bind
一、可变参数模版
C语言中有可变函数参数,比如我们熟悉的printf
和scanf
:
C++中有时候模版也需要可变参数,C++11的新特性可变参数模板可以接受可变参数的函数模板和类模板,而C++11之前的类模版和函数模版中只能含固定数量的模版参数。
下面是一个可变参数的函数模版:
template <class ...Args>void ShowList(Args... args){}
Args
是一个类型参数包,args
是一个函数形参参数包声明一个参数包Args...args
,这个参数包中可以包含0到任意个模板参数这里的可变指的是参数类型是任意的,参数个数也是任意的 这意味着我们可以像下面这样使用:
template <class ...Args>void ShowList(Args... args){}int main(){ShowList();ShowList(1);ShowList(1, 2.2);ShowList(1, 2.2, "3333");//...return 0;}
以前我们实现的模版只能接受多种类型,但是参数的个数却是确定的。可变参数模版不仅能接受多种类型,而且参数个数任意,因此可以看作是模版的模版。
这里实际是编译器帮我们生成了四个函数:ShowList()
、ShowList(int x)
、ShowList(int x, double y)
、ShowList(int x, double y, std::string& str)
。
我们也可以用sizeof()
看一下各个函数的参数个数:
只是这里sizeof()
的用法怎么看怎么奇怪,谁知道呢,人家确实是这么设计的。
如果我们想打印模版参数包怎么操作呢?这里有一个前提:它一定要在编译时推导参数。 我们无法直接获取参数包args
中的每个参数,只能通过展开参数包的方式,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。
有一个办法是用类似递归的方法处理:
void Print(){cout << endl;}template<class T, class ...Args>void Print(T&& x, Args&&... args){cout << x << " ";Print(args...);}template <class ...Args>void ShowList(Args&&... args){Print(args...);}int main(){ShowList(1, 2.2, "3333");return 0;}
注意上面args
的用法,除了sizeof
,...
都在args
的后面。
从上面的示例可以看到可变参数模版确实给我们提供了很大的方便的,但是麻烦事终归是不可避免的,那只能交给编译器来帮我们干这些繁琐的活了,感恩编译器。❤️
二、emplace_back
emplace_back
支持模板的可变参数,还有万能引用。
上面插入有名对象的使用场景中emplace_back
和push_back
是一样的。不一样的地方是插入匿名对象:emplace_back
支持用构建对象的参数自己去创建对象,省去了拷贝构造/移动构造这一步骤。
如果是push_back
插入匿名对象,它还是会先构造一个对象,再拿这个对象去拷贝构造目标对象。emplace_back
支持了模版的可变参数及万能引用,所以这个过程去掉了中间值,也就是第一步构造的对象,而是直接去构造目标对象。
| 实现自己的emplace_back:
只需要把参数包不断往下传递,最后根据参数包构造或拷贝构造目标对象。
传到最后如果参数包是已经存在的对象就调用对应的拷贝构造,如果是还未构建对象的参数就构造。
测试发现这里应该调移动构造啊,怎么调了构造构造呢?其中的原因在上篇文章中有介绍过,右值引用的本身是左值。 所以上面的参数包在传递过程中都需要用完美转发来保持原生类型属性。
以emplace_back
为例:
说实话这个使用的格式有点让人难受。
实现emplace_back
:
//...template<class... Args>list_node(Args&&... args):_data(forward<Args>(args)...),_next(nullptr),_prev(nullptr){}//...template<class... Args>void emplace_back(Args&&... args){insert(end(), forward<Args>(args)...);}//...template<class... Args>iterator insert(const iterator& pos, Args&&... args){Node* pcur = pos._node;Node* prev = pcur->_prev;Node* newnode = new Node(forward<Args>(args)...);prev->_next = newnode;newnode->_prev = prev;newnode->_next = pcur;pcur->_prev = newnode;++_size;return newnode;}//...
总结就是:如果插入有名对象push_back
和emplace_back
是一样的,但是插入匿名对象(构造对象的参数),push_back
还会先构造再拷贝构造,而emplace_back
是不断往下传递参数包,最后根据参数包中的数据来匹配是直接构造对象还是调用相应的拷贝构造。所以整体来说还是emplace_back
略胜一筹。
三、lambda表达式
如果我们要对自定义类型排序,就不能使用算法库中的sort
函数解决,只能自己用仿函数自定义排序规则:
struct Goods{Goods(const char* str, double price, int evaluate):_name(str),_price(price), _evaluate(evaluate){}string _name; //名字double _price; //价格int _evaluate; //评价};struct ComparePriceLess{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}};struct ComparePriceGreater{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}};int main(){vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());return 0;}
但这样有点繁琐,每次为了完成某种比较都要写一个类,如果自定义类型中的成员变量很多而且都要通过比较来排序,那就要实现很多个类。为此C++11提出了lambda
表达式来解决这个问题。
lambda
表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
[]
来判断下面的代码是否为lambda
函数,其作用是捕捉上下文中的变量供lambda
函数使用(parameters) :参数列表,和普通函数参数列表一样,如果不需要传参,则()
可省略mutable:默认lambda
函数是一个const
函数,mutable
可以取消其常性,使用时,参数列表总是不能省略-> return-type :返回值类型,没有返回值、返回值类型明确时可省略{ statement }:函数体,除了可以使用其参数外,还可使用所有捕获到的变量 lambda
表达式的类型没有名称,我们通常无法直接引用它,但可以使用auto
关键字来存储lambda
表达式的实例。
有了lambda
表达式,上面通过仿函数来实现排序可以写成:
int main(){vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; });sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._price > g2._price; });return 0;}
上述代码就是使用C++11中的lambda
表达式来解决,代码明显更简洁,可以看出lambda
表达式实际是一个匿名函数对象。
lambda
表达式捕捉列表说明:
其中:
父作用域指包含lambda
函数的语句块语法上捕捉列表可由多个捕捉项组成,并以逗号分割[=, &a, &b]
:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]
:值传递方式捕捉变量a和this,引用方式捕捉其他变量捕捉列表不允许变量重复传递,否则就会导致编译错误比如:
[=, a]
:=已经以值传递方式捕捉了所有变量,捕捉a重复在块作用域以外的lambda
函数捕捉列表必须为空在块作用域中的lambda
函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错lambda
表达式之间不能相互赋值,即使看起来类型相同允许使用一个lambda
表达式拷贝构造一个新的副本 仿函数是在类中重载了operator()
,使之能像函数一样可以调用。
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);// lambdaauto r2 = [=](double monty, int year)->double {return monty * rate * year;};r2(10000, 2);return 0;}
从上面的代码中可以看到仿函数的使用和lambda
函数的使用是一样的,那lambda
函数的底层是怎样的呢?
函数对象将rate作为其成员变量,在定义对象时给出初始值即可;lambda
表达式通过捕获列表可以直接将该变量捕获到,捕获的本质是构造函数初始化参数。
lambda
表达式的底层也是调用重载的operator()
.
四、包装器
4.1 function
使用function
需要包头文件<functional>
。
模板参数说明:
function
包装器也叫作适配器。C++中的function
本质是一个类模板,也是一个包装器。function
可包装任何类型的可调用对象:函数指针、仿函数、lambda
。
int func(int a, int b){return a + b;}struct Func{int operator()(int a, int b){return a + b;}};int main(){//包装可调用对象function<int(int, int)> f1 = func;function<int(int, int)> f2 = Func();function<int(int, int)> f3 = [](int a, int b) {return a + b; };cout << f1(1, 2) << endl;cout << f2(1, 2) << endl;cout << f3(1, 2) << endl;return 0;}
从汇编层可以看到,function
包装器仅仅是在外面进行了包装,其底层都还是调用的operator()
。
function
的特点可以用来做类型统一,比如在下面的这个例题中:
class Solution {public: int evalRPN(vector<string>& tokens) { stack<int> _st; for (auto& e : tokens) { if (e == "+" || e == "-" || e == "*" || e == "/") { int a = _st.top(); _st.pop(); int b = _st.top(); _st.pop(); switch(e[0]) { case '+': _st.push(b + a); break; case '-': _st.push(b - a); break; case '*': _st.push(b * a); break; case '/': _st.push(b / a); break; default: break; } } else { _st.push(stoi(e)); } } return _st.top(); }};
可以看出这道题的关键就是遇到运算符就执行对应的运算操作,我们可以用map+function+lambda
进行简单的包装,从而得出这个题的新玩法:
class Solution {public: int evalRPN(vector<string>& tokens) { stack<int> st; map<string, function<int(int, int)>> func_map = { {"+", [](int a, int b){return a + b;}} ,{"-", [](int a, int b){return a - b;}} ,{"*", [](int a, int b){return a * b;}} ,{"/", [](int a, int b){return a / b;}} }; for (auto& str : tokens) { if (func_map.find(str) != func_map.end()) { int a = st.top(); st.pop(); int b = st.top(); st.pop(); st.push(func_map[str](b, a)); } else { st.push(stoi(str)); } } return st.top(); }};
另外,使用function
包装可调用对象时,类型一定要匹配,类型不匹配就会报错。在这个点上特别需要注意的是类的成员函数。比如:
class Func{public:static int func1(int a, int b){return a + b;}//非静态成员函数double func2(double a, double b){return a + b;}};int main(){function<int(int, int)> f1 = &Func::func1;//静态成员函数function<double(double, double)> f2 = &Func::func2;//非静态成员函数return 0;}
成员函数受类域限制,需要指定类域非静态成员函数取函数指针时要+&
符号,静态成员函数可以不加,但建议还是都加上非静态成员函数在类内部取函数指针也需要指定类域,静态成员函数就不需要 上面的包装有错误,你能发现吗?
事实上上面的包装有一处是类型不匹配的,就是对非静态成员函数的包装。用function
包装非静态成员函数时不要忘了它还有一个隐含的this指针,而静态成员函数是没有this指针的。 包装非静态成员函数下面两种方式都可以:
//方式一:function<double(Func*, double, double)> f2 = &Func::func2;Func f;cout << f2(&f, 1.1, 2.2) << endl;//方式二:function<double(Func, double, double)> f3 = &Func::func2;cout << f3(Func(), 1.1, 2.2) << endl;
为什么一种是对象的指针,另一种直接是对象呢?因为这里并不是直接把对象指针或者对象传给函数func2,事实上this指针也不支持这样传,function
的底层还是调用的operator()
,而不管是指针还是对象都可以调用成员函数。
4.2 bind
bind
函数也是定义在头文件<functional>
中,是一个函数模版,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用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为第二个参数,以此类推。
1、调整参数顺序(不常用)
2、调整参数个数(常用)
再看上面function
包装非静态成员函数,每次调用都要传对象或对象指针有点麻烦,可以用bind
来绑死这个固定参数。
bind
本质返回一个仿函数对象,因此也可以用function
包装。
//方式二:function<double(Func, double, double)> f3 = &Func::func2;cout << f3(Func(), 1.1, 2.2) << endl;function<double(double, double)> f4 = bind(&Func::func2, Func(), _1, _2);cout << f4(1.1, 2.2) << endl;
本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~