【C++练级之路】【Lv.23】C++11——可变参数模板、lambda表达式和函数包装器

快乐的流畅 2024-06-18 15:05:03 阅读 64

快乐的流畅:个人主页

个人专栏:《算法神殿》《数据结构世界》《进击的C++》

远方有一堆篝火,在为久候之人燃烧!

文章目录

一、可变参数模板1.1 参数包的概念1.2 参数包的展开1.3 emplace系列 二、lambda表达式2.1 lambda的格式2.2 捕捉列表2.3 lambda的原理2.4 lambda的优势 三、函数包装器3.1 function3.2 bind

一、可变参数模板

C++11更新后,可以创建接受可变参数的函数模板和类模板。

1.1 参数包的概念

以下是基本可变参数的函数模板:

template <class... Args>void ShowList(Args... args){ cout << sizeof...(args) << endl;}void test(){ ShowList(); ShowList(1); ShowList(1, 2.4); ShowList(1, 2.4, 'g'); ShowList(1, 2.4, 'g', 3.56);} Args是一个模板参数包,args是一个函数参数包参数包中可以包含0到任意个模板参数sizeof…运算符可以获取可变参数模板中参数的数量

ps:参数前面有省略号,就是一个可变模版参数,我们把带省略号的参数称为“参数包”

ps:对于可变参数模板,编译器会从函数的实参推断模板参数类型。同时,编译器还会推断包中参数的数目。

1.2 参数包的展开

可变模版参数的一个主要特点,便是无法直接获取参数包中的每个参数,所以只能通过展开参数包的方式来获取参数包中的每个参数。


递归方式:

// 递归终止函数void _ShowList(){ cout << endl;}// 展开函数template <class T, class... Args>void _ShowList(const T& val, Args... args){ cout << val << " ";_ShowList(args...);}template <class... Args>void ShowList(Args... args){ _ShowList(args...);}

利用子函数_ShowList每次获取参数包的第一个元素并打印,然后继续传递参数包,直到参数包没有参数,调用递归终止函数。


数组方式:

template<class T>int PrintArgs(T val){ cout << val << " ";return 0;}template<class... Args>void ShowList(Args... args){ int arr[] = { PrintArgs(args)...};cout << endl;}

利用数组初始化的过程展开参数包并打印。

以下有一种更简洁的写法,运用了逗号表达式和折叠表达式(C++17)

template<class T>void PrintArgs(T val){ cout << val << " ";}template<class... Args>void ShowList(Args... args){ (PrintArgs(args), ...);cout << endl;}

1.3 emplace系列

STL容器中emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对于insert系列,emplace系列接口的优势到底在哪里呢?

接口展示:

//emplace系列template <class... Args> void emplace_back (Args&&... args);//insert系列template <class T>void push_back (const T& val);void push_back (T&& val);

单参数:

void test(){ list<my::string> lt;//此处运用my::string,便于调试和观察my::string s1 = "1111";lt.push_back(s1);lt.push_back(move(s1));my::string s2 = "2222";lt.emplace_back(s2);lt.emplace_back(move(s2));cout << endl;lt.push_back("3333");lt.emplace_back("3333");}

其实,插入s1和s2的过程没有任何区别,有一个细微的区别在于直接插入"3333"时,push_back是构造+移动构造,而emplace_back是构造,相比之下只是少了一个移动构造,差别不大。

那么,为什么emplace_back是构造呢?因为参数包层层往下传递,直到节点的构造函数,在初始化列表中才解析出具体类型,所以就可以直接在节点上进行构造


多参数:

void test(){ list<pair<my::string, int>> lt;lt.push_back(make_pair("1111", 1));lt.push_back({ "2222",2 });lt.emplace_back(make_pair("3333", 3));lt.emplace_back("4444", 4);}

因为emplace_back的形参是参数包,所以可以写成多参数的形式传入

二、lambda表达式

2.1 lambda的格式

lambda表达式书写格式

[capture-list] (parameters) mutable -> return-type { statement } [capture-list] : 捕捉列表。捕捉列表能够捕捉上下文中的变量供lambda函数使用。(parameters):参数列表。mutable:修饰符。mutable可以取消其常量性,让传值捕捉的变量(默认为const)可以被修改。->returntype:返回值类型。{statement}:函数体。在该函数体内,可以使用参数和捕获的变量


ps:若不需要传参,则可省略参数列表。

ps:使用mutable修饰,则不可省略参数列表。

ps:返回值类型明确时,可省略,编译器自动推导。

2.2 捕捉列表

捕捉方式分两种,传值捕捉和传引用捕捉。

传值捕捉:

void test(){ int x = 1, y = 2;//[var]auto f1 = [x, y] { cout << x << " " << y << endl; };//[=]auto f2 = [=] { cout << x << " " << y << endl; };}class A{ public:void print(){ //[this]auto f3 = [this] { cout << _a1 << " " << _a2 << endl; };}private:int _a1, _a2;}; [var]:传值捕捉var变量[=]:传值捕捉所有变量(包括this)[this]:传值捕捉this指针

传引用捕捉:

void test(){ int x = 1, y = 2;//[&var]auto f1 = [&x, &y] { cout << x << " " << y << endl; };//[&]auto f2 = [&] { cout << x << " " << y << endl; };} [&var]:传引用捕捉var变量[&]:传引用捕捉所有变量(包括this)


ps:可以混合捕捉,但不能重复捕捉。

ps:只能捕捉父作用域中的局部变量(父作用域,指包含lambda的语句块)。

2.3 lambda的原理

lambda表达式,底层原理就是仿函数(类似于范围for的底层是迭代器)。

先看看以下代码:

void test(){ auto f1 = [](int x) { cout << x << endl; };f1(1);cout << typeid(f1).name() << endl;auto f2 = [](int x) { cout << x << endl; };f2(2);cout << typeid(f2).name() << endl;}

对于用户,lambda是匿名函数对象,所以用auto接收。但是即使定义完全相同的两个lambda,其函数类型还是不同。所以 lambda表达式之间不能相互赋值,它们之间的类型是互不相同的。


再看看以下代码:

class Rate{ public:Rate(double rate) : _rate(rate){ }double operator()(double money, int year){ return money * _rate * year;}private:double _rate;};void test(){ double rate = 0.49;// 函数对象Rate r1(rate);r1(10000, 2);// lambdaauto r2 = [=](double monty, int year)->double { return monty * rate * year;};r2(10000, 2);}

使用方式相同:

函数对象将rate作为其成员变量,在定义对象时给出初始值即可。lambda表达式通过捕获列表可以直接捕获到该变量。

底层实现相同:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

2.4 lambda的优势

对于一个商品类Goods:

struct Goods{ string _name;double _price;int _evaluate;Goods(const char* str, double price, int evaluate): _name(str), _price(price), _evaluate(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;}};void test(){ 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());}

lambda表达式:

void test(){ 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; });}

仿函数和lambda表达式比较:

仿函数的方式比较笨重,且类名必须取的清晰,否则容易混淆不清。lambda表达式则简洁清晰,在原本传参的位置定义,可以清楚知道比较的方式。

三、函数包装器

目前,我们学习了三种回调函数的方式:函数指针、仿函数和lambda表达式。而在传参时,函数模板会因不同的类型而产生多份实例化,导致效率低下。所以,我们希望有统一的类型可以接收可调用对象,以达到包装的目的,提高效率。

3.1 function

function是一个模板类,提供了一种通用的、多态的函数封装,是一个函数包装器(适配器)

template <class T> function; // undefinedtemplate <class Ret, class... Args> class function<Ret(Args...)>;

ps:Ret是被调用函数的返回类型,Args是被调用函数的参数类型。


请看看以下代码:

void Swap(int& x, int& y){ int tmp = x;x = y;y = tmp;}struct SwapFunctor{ void operator()(int& x, int& y){ int tmp = x;x = y;y = tmp;}};void test(){ //函数指针function<void(int&, int&)> f1 = Swap;//函数对象function<void(int&, int&)> f2 = SwapFunctor();//lambda表达式function<void(int&, int&)> f3 = [](int& x, int& y){ int tmp = x;x = y;y = tmp;};}

function提供了统一的类型来接收不同类型的可调用对象,包括函数指针、仿函数和lambda表达式等,实现了函数包装。


比较特殊的,是function接收类的成员函数:

class Plus{ public:static int plusi(int x, int y){ return x + y;}double plusd(double x, double y){ return x + y;}};int main(){ //类的静态成员函数function<int(int, int)> f1 = Plus::plusi;//类的普通成员函数function<double(Plus*, double, double)> f2 = &Plus::plusd;Plus p;f2(&p, 1.1, 2.2);function<double(Plus, double, double)> f3 = &Plus::plusd;f3(Plus(), 1.1, 2.2);return 0;} function接收类的成员函数,要& + 类域(静态成员函数不用&)。因为类的成员函数有隐含的参数this指针,所以function内部的类型要加上类指针。由于传入指针比较麻烦,所以编译器做了特殊处理,可以传入类。


leetcode 150.逆波兰表达式求值

逆波兰表达式(function化简版):

class Solution{ public: int evalRPN(vector<string>& tokens) { stack<int> st; unordered_map<string, function<int(int, int)>> hash = { { "+", [](int x, int y){ return x + y;}}, { "-", [](int x, int y){ return x - y;}}, { "*", [](int x, int y){ return x * y;}}, { "/", [](int x, int y){ return x / y;}} }; for(auto& str : tokens) { if(hash.count(str)) { int right = st.top();st.pop(); int left = st.top();st.pop(); st.push(hash[str](left, right)); } else st.push(stoi(str)); } return st.top(); }};

3.2 bind

bind是一个模板函数,可以接收一个可调用对象,进行函数参数绑定,返回一个绑定后的对象

//simple(1)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);

ps:fn是可调用对象,args是需要进行绑定的参数列表。


bind的绑定方式分两种,绑定值和绑定占位符

int Sub(int x, int y){ return x - y;}void test(){ function<int(int, int)> f1 = Sub;f1(10, 5);//绑定值——调整参数个数function<int()> f2 = bind(Sub, 20, 10);f2();//绑定占位符——调整参数顺序function<int(int, int)> f3 = bind(Sub, placeholders::_2, placeholders::_1);f3(10, 5);//同时绑定值和占位符function<int(int)> f4 = bind(Sub, 20, placeholders::_1);f4(10);} placeholders是与bind一起使用的工具,用于指定绑定表达式中的占位符。_1代表函数调用实参的第一个位置,依此类推至 _n。每次函数调用时,传入的实参会对应到bind的参数列表,再传入调用的函数。绑定值后,function内的类型要相应的变化(参数类型个数要减少)


对于之前function接收类的成员函数,我们可以用bind进行化简:

class Plus{ public:double plusd(double x, double y){ return x + y;}};void test(){ //function<double(Plus, double, double)> f3 = &Plus::plusd;//f3(Plus(), 1.1, 2.2);function<double(double, double)> f3 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);f3(1.1, 2.2);}

运用bind将Plus()参数固定绑死,这样在传参时就不用每次传入,变得更加简洁。

真诚点赞,手有余香


声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。