免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

【C++】C++11新特性—可变参数模版、lambda、包装器

【C++】C++11新特性—可变参数模版、lambda、包装器 目录一、可变参数模版二、emplace_back三、lambda表达式四、包装器4.1 function4.2 bind一、可变参数模版C语言中有可变函数参数比如我们熟悉的printf和scanfC中有时候模版也需要可变参数C11的新特性可变参数模板可以接受可变参数的函数模板和类模板而C11之前的类模版和函数模版中只能含固定数量的模版参数。下面是一个可变参数的函数模版templateclass...ArgsvoidShowList(Args...args){}Args是一个类型参数包args是一个函数形参参数包声明一个参数包Args...args这个参数包中可以包含0到任意个模板参数这里的可变指的是参数类型是任意的参数个数也是任意的这意味着我们可以像下面这样使用templateclass...ArgsvoidShowList(Args...args){}intmain(){ShowList();ShowList(1);ShowList(1,2.2);ShowList(1,2.2,3333);//...return0;}以前我们实现的模版能接受多种类型但是参数的个数却是确定的。可变参数模版不仅能接受多种类型而且参数个数任意因此可以看作是模版的模版。这里实际是编译器帮我们生成了四个函数ShowList()、ShowList(int x)、ShowList(int x, double y)、ShowList(int x, double y, std::string str)。我们也可以用sizeof()看一下各个函数的参数个数这里sizeof()的用法怎么看怎么奇怪谁知道呢人家确实是这么设计的。如果我们想打印模版参数包怎么操作呢这里有一个前提它一定要在编译时推导参数。我们无法直接获取参数包args中的每个参数只能通过展开参数包的方式这是使用可变模版参数的一个主要特点也是最大的难点即如何展开可变模版参数。有一个办法是用类似递归的方法处理voidPrint(){coutendl;}templateclassT,class...ArgsvoidPrint(Tx,Args...args){coutx ;Print(args...);}templateclass...ArgsvoidShowList(Args...args){Print(args...);}intmain(){ShowList(1,2.2,3333);return0;}注意上面args的用法除了sizeof...都在args的后面。从上面的示例可以看到可变参数模版确实给我们提供了很大的方便的但是麻烦事终归是不可避免的那只能交给编译器来帮我们干这些繁琐的活了感恩编译器。❤️二、emplace_backemplace_back支持模板的可变参数还有万能引用。上面插入有名对象的使用场景中emplace_back和push_back是一样的。不一样的地方是插入匿名对象emplace_back支持用构建对象的参数自己去创建对象省去了拷贝构造/移动构造这一步骤。如果是push_back插入匿名对象它还是会先构造一个对象再拿这个对象去拷贝构造目标对象。emplace_back支持了模版的可变参数及万能引用所以这个过程去掉了中间值也就是第一步构造的对象而是直接去构造目标对象。| 实现自己的emplace_back只需要把参数包不断往下传递最后根据参数包构造或拷贝构造目标对象。传到最后如果参数包是已经存在的对象就调用对应的拷贝构造如果是还未构建对象的参数就构造。测试发现这里应该调移动构造啊怎么调了构造构造呢其中的原因在上篇文章中有介绍过右值引用的本身是左值。所以上面的参数包在传递过程中都需要用完美转发来保持原生类型属性。以emplace_back为例说实话这个使用的格式有点让人难受。实现emplace_back://...templateclass...Argslist_node(Args...args):_data(forwardArgs(args)...),_next(nullptr),_prev(nullptr){}//...templateclass...Argsvoidemplace_back(Args...args){insert(end(),forwardArgs(args)...);}//...templateclass...Argsiteratorinsert(constiteratorpos,Args...args){Node*pcurpos._node;Node*prevpcur-_prev;Node*newnodenewNode(forwardArgs(args)...);prev-_nextnewnode;newnode-_prevprev;newnode-_nextpcur;pcur-_prevnewnode;_size;returnnewnode;}//...总结就是如果插入有名对象push_back和emplace_back是一样的但是插入匿名对象构造对象的参数push_back还会先构造再拷贝构造而emplace_back是不断往下传递参数包最后根据参数包中的数据来匹配是直接构造对象还是调用相应的拷贝构造。所以整体来说还是emplace_back略胜一筹。三、lambda表达式如果我们要对自定义类型排序就不能使用算法库中的sort函数解决只能自己用仿函数自定义排序规则structGoods{Goods(constchar*str,doubleprice,intevaluate):_name(str),_price(price),_evaluate(evaluate){}string _name;//名字double_price;//价格int_evaluate;//评价};structComparePriceLess{booloperator()(constGoodsgl,constGoodsgr){returngl._pricegr._price;}};structComparePriceGreater{booloperator()(constGoodsgl,constGoodsgr){returngl._pricegr._price;}};intmain(){vectorGoodsv{{苹果,2.1,5},{香蕉,3,4},{橙子,2.2,3},{菠萝,1.5,4}};sort(v.begin(),v.end(),ComparePriceLess());sort(v.begin(),v.end(),ComparePriceGreater());return0;}但这样有点繁琐每次为了完成某种比较都要写一个类如果自定义类型中的成员变量很多而且都要通过比较来排序那就要实现很多个类。为此C11提出了lambda表达式来解决这个问题。lambda表达式书写格式[capture-list] (parameters) mutable - return-type { statement }[capture-list]捕捉列表编译器根据[]来判断下面的代码是否为lambda函数其作用是捕捉上下文中的变量供lambda函数使用(parameters)参数列表和普通函数参数列表一样如果不需要传参则()可省略mutable默认lambda函数是一个const函数mutable可以取消其常性使用时参数列表总是不能省略- return-type返回值类型没有返回值、返回值类型明确时可省略{ statement }函数体除了可以使用其参数外还可使用所有捕获到的变量lambda表达式的类型没有名称我们通常无法直接引用它但可以使用auto关键字来存储lambda表达式的实例。有了lambda表达式上面通过仿函数来实现排序可以写成intmain(){vectorGoodsv{{苹果,2.1,5},{香蕉,3,4},{橙子,2.2,3},{菠萝,1.5,4}};sort(v.begin(),v.end(),[](constGoodsg1,constGoodsg2){returng1._priceg2._price;});sort(v.begin(),v.end(),[](constGoodsg1,constGoodsg2){returng1._priceg2._price;});return0;}上述代码就是使用C11中的lambda表达式来解决代码明显更简洁可以看出lambda表达式实际是一个匿名函数对象。lambda表达式捕捉列表说明[var]表示值传递方式捕捉变量var[]表示值传递方式捕获所有父作用域中的变量(包括this)[var]表示引用传递捕捉变量var[]表示引用传递捕捉所有父作用域中的变量(包括this)其中父作用域指包含lambda函数的语句块语法上捕捉列表可由多个捕捉项组成并以逗号分割[, a, b]以引用传递的方式捕捉变量a和b值传递方式捕捉其他所有变量[a, this]值传递方式捕捉变量a和this引用方式捕捉其他变量捕捉列表不允许变量重复传递否则就会导致编译错误比如[, a]已经以值传递方式捕捉了所有变量捕捉a重复在块作用域以外的lambda函数捕捉列表必须为空在块作用域中的lambda函数仅能捕捉父作用域中局部变量捕捉任何非此作用域或者非局部变量都 会导致编译报错lambda表达式之间不能相互赋值即使看起来类型相同允许使用一个lambda表达式拷贝构造一个新的副本仿函数是在类中重载了operator()使之能像函数一样可以调用。classRate{public:Rate(doublerate):_rate(rate){}doubleoperator()(doublemoney,intyear){returnmoney*_rate*year;}private:double_rate;};intmain(){// 函数对象doublerate0.49;Rater1(rate);r1(10000,2);// lambdaautor2[](doublemonty,intyear)-double{returnmonty*rate*year;};r2(10000,2);return0;}从上面的代码中可以看到仿函数的使用和lambda函数的使用是一样的那lambda函数的底层是怎样的呢函数对象将rate作为其成员变量在定义对象时给出初始值即可lambda表达式通过捕获列表可以直接将该变量捕获到捕获的本质是构造函数初始化参数。lambda表达式的底层也是调用重载的operator().四、包装器4.1 function使用function需要包头文件functional。模板参数说明Ret: 被调用函数的返回类型Args…被调用函数的形参function包装器也叫作适配器。C中的function本质是一个类模板也是一个包装器。function可包装任何类型的可调用对象函数指针、仿函数、lambda。intfunc(inta,intb){returnab;}structFunc{intoperator()(inta,intb){returnab;}};intmain(){//包装可调用对象functionint(int,int)f1func;functionint(int,int)f2Func();functionint(int,int)f3[](inta,intb){returnab;};coutf1(1,2)endl;coutf2(1,2)endl;coutf3(1,2)endl;return0;}从汇编层可以看到function包装器仅仅是在外面进行了包装其底层都还是调用的operator()。function的特点可以用来做类型统一比如在下面的这个例题中逆波兰表达式求值classSolution{public:intevalRPN(vectorstringtokens){stackint_st;for(autoe:tokens){if(e||e-||e*||e/){inta_st.top();_st.pop();intb_st.top();_st.pop();switch(e[0]){case:_st.push(ba);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();}};可以看出这道题的关键就是遇到运算符就执行对应的运算操作我们可以用mapfunctionlambda进行简单的包装从而得出这个题的新玩法classSolution{public:intevalRPN(vectorstringtokens){stackintst;mapstring,functionint(int,int)func_map{{,[](inta,intb){returnab;}},{-,[](inta,intb){returna-b;}},{*,[](inta,intb){returna*b;}},{/,[](inta,intb){returna/b;}}};for(autostr:tokens){if(func_map.find(str)!func_map.end()){intast.top();st.pop();intbst.top();st.pop();st.push(func_map[str](b,a));}else{st.push(stoi(str));}}returnst.top();}};另外使用function包装可调用对象时类型一定要匹配类型不匹配就会报错。在这个点上特别需要注意的是类的成员函数。比如classFunc{public:staticintfunc1(inta,intb){returnab;}//非静态成员函数doublefunc2(doublea,doubleb){returnab;}};intmain(){functionint(int,int)f1Func::func1;//静态成员函数functiondouble(double,double)f2Func::func2;//非静态成员函数return0;}成员函数受类域限制需要指定类域非静态成员函数取函数指针时要符号静态成员函数可以不加但建议还是都加上非静态成员函数在类内部取函数指针也需要指定类域静态成员函数就不需要上面的包装有错误你能发现吗事实上上面的包装有一处是类型不匹配的就是对非静态成员函数的包装。用function包装非静态成员函数时不要忘了它还有一个隐含的this指针而静态成员函数是没有this指针的。包装非静态成员函数下面两种方式都可以//方式一functiondouble(Func*,double,double)f2Func::func2;Func f;coutf2(f,1.1,2.2)endl;//方式二functiondouble(Func,double,double)f3Func::func2;coutf3(Func(),1.1,2.2)endl;为什么一种是对象的指针另一种直接是对象呢因为这里并不是直接把对象指针或者对象传给函数func2事实上this指针也不支持这样传function的底层还是调用的operator()而不管是指针还是对象都可以调用成员函数。4.2 bindbind函数也是定义在头文件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包装。//方式二functiondouble(Func,double,double)f3Func::func2;coutf3(Func(),1.1,2.2)endl;functiondouble(double,double)f4bind(Func::func2,Func(),_1,_2);coutf4(1.1,2.2)endl;bind绑定成员函数#includefunctional#includeiostreamclassMyClass{public:voidmemberFunction(intx){std::coutMember function called with x x and this thisstd::endl;}voidsetupCallback(){// 使用std::bind绑定memberFunction并传递this指针autoboundFuncstd::bind(MyClass::memberFunction,this,std::placeholders::_1);// 现在boundFunc是一个可调用对象它可以在稍后调用并且会调用当前对象的memberFunctionboundFunc(42);}};intmain(){MyClass obj;obj.setupCallback();// 输出: Member function called with x 42 and this 对象的地址return0;}当你使用std::bind来创建一个指向成员函数的绑定时你需要显式地提供这个this指针因为std::bind无法自动获取它。这是因为在std::bind的调用时刻它并不知道你将如何或何时使用这个绑定所以它无法知道哪个对象实例应该被使用。本篇文章的分享就到这里了如果您觉得在本文有所收获还请留下您的三连支持哦~
返回列表