目录
可变参数模板
引入
介绍
展开参数包的方法
递归
逗号表达式
整体使用
emplace
介绍
编辑
使用
模拟实现
代码
示例
lambda
引入
介绍
格式
使用
传参
捕捉
原理
可变参数模板
引入
- 还记得c语言中的printf吗,可以传入任意数量的变量来打印,非常的便捷
- 而他正是用了c中的可变参数列表,不过和我们现在要介绍的可变参数模板的原理不一样
- 我们只要知道可变参数模板很好用很灵活就是了,它让泛型编程更加完善
- 但是,c++中的这个语法很抽象,学个基本也就差不多了
介绍
template <class ...Args> void ShowList(Args... args) {}
- 带...的参数称为"参数包",包含了0-n个模板参数
- Args是传入的模板参数包,而args是函数形参参数包
- 但我们无法像printf里那样做,printf是通过va_list对象和宏拿到它的参数的
- 也无法使用args[i]来取参数
- 这里,只能通过展开参数包来拿到参数
展开参数包的方法
递归
// 递归终止函数 template <class T> void ShowList(const T& t) //当只有一个参数时 { cout << t << endl; } // 展开函数 template <class T, class ...Args> void ShowList(T value, Args... args) //多个参数时 { cout << value <<" "; ShowList(args...); }
这里利用了函数重载,当调用showlist函数时:
- 如果传入了多个参数,会匹配第二个函数,并且不断调用自己,拿到自己当前的第一个模板参数,这样,传入参数包的参数会不断减少一个,直到最后只剩一个模板参数时,调用第一个函数
- 如果只传入一个参数,刚好就直接匹配到第一个函数(会找到最匹配的那个)
- 如果要支持无参调用,可以将第一个函数改为无参(因为args可以是0个参数)
逗号表达式
它不需要递归来获取,而是利用多个语法的特性,直接数组构造过程中把参数包展开
template <class T> void PrintArg(T t) { cout << t << " "; } //展开函数 template <class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; cout << endl; }
- 这里是用初始化列表初始化一个数组,而里面的元素是逗号表达式+参数包
- 所以,实际上它里面的元素展开后是 -- ((printarg(arg1),0), (printarg(arg2),0),(printarg(arg3),0)...)
- 所以在构造的过程中,会一个一个执行逗号表达式
- 首先,先执行逗号前的表达式,也就是调用函数,打印参数
- 然后用逗号后的int值--也就是这里的0,作为该逗号表达式的返回值,初始化数组
- 这样,一个数组构造完毕,参数包里的参数也都打印出来了
整体使用
但是,上面两种方法只能一个一个取出参数,没啥大用,最多就是打印出来
但是,如果我们能整体使用,就可以用来传参了!
class Date { public: Date(int year = 1, int month = 1, int day = 1) :_year(year) ,_month(month) ,_day(day) { cout << "Date构造" << endl; } Date(const Date& d) :_year(d._year) , _month(d._month) , _day(d._day) { cout << "Date拷贝构造" << endl; } private: int _year; int _month; int _day; }; template <class ...Args> Date* Create(Args... args) { Date* ret = new Date(args...); return ret; }
- create函数中,将传入的多个参数打包成参数包,直接用来构造date对象
- 而参数包中的参数,可以用来匹配函数的参数,如果匹配,就可以直接调用该函数
- 如果不匹配,就会报错
- 而这个特性,就是emplace的基础
emplace
介绍
- emplace系列的这两个函数,在多个stl容器中都有设置,而他,正使用了上面介绍的可变参数模板来插入任意个数个元素
使用
这里我们用list的结点为pair来演示可变参数列表的作用
void test1() { std::list<std::pair<int, bit::mystring>> l; l.push_back(make_pair(1, "22")); l.push_back({1, "1434"}); cout << endl; l.emplace_back(1, "111"); }
- 在之前,我们可以使用make_pair/多参数的隐式类型转换 来传入pair对象
- 而emplace系列可以直接传参
- 因为push_back的参数只能是元素类型,也就是这里的pair类型
- 所以,传入的非pair类型的参数,都得先构造一个临时的pair对象,才能进入该函数
- 所以 --
- 这里结果的上半截,都是先构造一个string,然后构造pair
- 再在push_back内部,用pair调用结点的构造函数
- 在构造函数中,又分别调用pair对象的first成员,second成员的拷贝构造,拷贝出一个pair对象给结点
- 但又因为有移动拷贝的存在,所以,拷贝给结点的过程是用移动操作完成的
- 结果的下半截则是用可变参数模板接收,和上面介绍的整体使用一样,参数包一直被传递到调用pair的拷贝构造那里
- 然后,传入的参数个数和类型刚好能匹配pair的构造函数
- 所以,就直接调用了string的构造,用传入的字符串初始化了pair的second成员
- 这样就绕过拷贝操作,直接构造最底层调用
其实这里用自定义类型的话,效率差别不大,因为有移动操作的存在,它可以直接交换资源
但是,如果涉及到占用大内存,但全是内置类型的类,用emplace系列的函数,会大大提高效率(就类似上面的date类)
模拟实现
我们可以在自己实现的list中,也添加emplace函数
代码
template <class Data> ListNode(Data &&val) //之前实现过的移动构造 : _ppre(nullptr), _pnext(nullptr), _val(forward<Data>(val)){}; template <class... Args> ListNode(Args... args) : _ppre(nullptr), _pnext(nullptr), _val(args...) //这样这里将可变参数模板直接传入构造val的构造函数里,如果匹配,就直接构造了 { } template <class... Args> void emplace_back (Args&&... args){ insert(end(),args...); //将参数包传给insert } template <class... Args> iterator insert(iterator pos, Args&&... args) { PNode cur = pos._pNode; PNode pre = cur->_ppre; PNode newnode = new Node(args...); //参数包传给node构造函数 newnode->_pnext = cur; pre->_pnext = newnode; cur->_ppre = newnode; newnode->_ppre = pre; _size++; return newnode; }
示例
这里上半截我们比库里的多了两行
原因在于我们这里构造头结点的时候,也需要用构造string,只不过是默认构造;而库里是用的内存池
lambda
引入
我们之前已经学习了很多调用函数的方式,比如:函数指针,仿函数(实际上是类重载了())
- 但是,这意味着,我们每使用一次函数,就要写一次函数体
- 如果我们需要多次使用,功能类似但不同的函数该怎么办?定义很多份吗?
- 这就会涉及到函数起名的问题
- 功能类似真的好难取名,如果不好好取名,可读性就很差
而这里的lambda表达式,也可以像函数一样使用,它就能解决这个问题
介绍
是C++11引入的一种匿名函数或闭包表达式,它允许你在代码中内联定义函数,而无需显式命名函数
格式
[capture-list] (parameters) mutable -> return-type { statement }
[capture-list]
- 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数
- 捕捉列表能够捕捉上下文中的变量供lambda函数使用
- 默认情况下,捕捉到的是该变量的拷贝,且为const类型
(parameters)
- 参数列表,与普通函数的参数列表的使用一致
- 如果不需要参数传递,可以连同()一起省略
mutable
- 因为前面已经介绍了,lambda函数默认下,捕捉到的变量是const类型,而mutable可以取消其常量性(但注意,这里只是没有了常量性,但依然是变量的拷贝)
- 使用该修饰符时,参数列表不可省略(即使参数为空)
->returntype
- 返回值类型,用追踪返回类型形式声明函数的返回值类型
- 没有返回值/返回值明确时可省略(因为编译器可以对返回类型进行推导,就像auto那样)
{statement}
- 函数体
- 在该函数体内,可以使用参数/捕获到的对象/全局域的函数,其他的无法使用
使用
传参
可以传值,也可以传引用
auto goodsPriceLess = [](const Goods& x, const Goods& y){return x._price < y._price; };
捕捉
默认下捕捉到的是const类型的拷贝
- 可以使用mutable改变const类型,但依然是拷贝
- 也可以直接传引用(&变量名),这样x既能修改,也能在外部得到这份修改:
int main() { int x = 0; auto a = [&x]() {++x; }; a(); cout << x << endl; return 0; }
如果想要捕捉到该作用域的全部变量时,可以使用[ = ] / [ & ]
[ = ] -- 用值捕捉到该作用域的所有变量
[ & ] -- 用引用捕捉到该作用域的所有变量
注意!在块作用域之外的lambda表达式,捕捉列表必须为空
原理
- 实际上,我们的lambda表达式之间是无法赋值的,即使是完全一样的表达式
- 而原因就是因为,它的底层是仿函数,每一个表达式都是不同的类对象,且类中重载了()运算符
- 所以,每一个lambda表达式都是不同的类型,所以无法赋值
- 但是,它支持拷贝
- lambda表达式都是匿名类对象,他们的名字和类型都是由编译器自动生成的
- 名字是lambda+uuid(uuid是用了某种算法,生成的重复概率极小的字符串)
- 而类型则是一个匿名的函数对象类型
- 使用lambda表达式就是调用类中的operator()函数
- 如果一个函数指针的类型和lambda表达式类型是一样的,则lambda 表达式可以分配给函数指针(可能因为operator()函数具有与函数指针相似的签名(参数列表和返回类型))
举例
sort(v.begin(), v.end(), [](const Goods& x, const Goods& y) {return x._price > y._price;});
可以传给算法,比如sort中的排序,这样就不用自己专门写一个仿函数了
function包装器(适配器)
引入
现在,我们已经知道了有三种调用函数的方式 -- 函数指针,仿函数,lambda表达式
那么使用类模板传参的时候,如果分别用了三种不同方式的函数,就会实例化出三份,但实际我们并不需要这么多份
而且如果我们想要将这些可调用类型存在容器中呢?实例化容器的时候类型该怎么写呢?函数指针和仿函数的类型都能写,但lambda的该怎么办呢?
所以,function包装器可以解决这个问题,它可以适配所有可调用的类型
介绍
头文件:<functional>
无需在编译时指定确切的类型,而是在运行时将不同类型的可调用对象分配给它
我们可以用function定义出的对象接收不同类型的可调用对象,只要返回值和参数列表相同即可:
格式
functioni<返回值(参数列表)>
使用
int func(int x, int y) { return x - y; } struct Function { int operator()(int x, int y) { return x - y; } }; void test2() { function<int(int, int)> sub1 = [](int x, int y) {return x - y; }; function<int(int, int)> sub2 = func; function<int(int, int)> sub3 = Function(); cout << sub1(2, 1) << endl; cout << sub2(3, 1) << endl; cout << sub3(4, 1) << endl; }
和原先使用他们的方式一样,只不过接收它的是function类型
存放在容器中:
void test3() { vector< function<int(int, int)>> f; f.push_back(func); f.push_back(Function()); f.push_back([](int x, int y) {return x - y; }); cout << f[0](1, 2) << endl; }
当我们存起来后,可以通过下标拿到可调用对象,再使用它执行函数功能
除此之外,可以用来指定命令执行某一函数
比如:当我们已经有了后缀表达式时,就可以直接进行计算,但是得分很多种情况,运算符越多,情况也越多
- 如果直接使用判断的方式,效率太低
- 我们可以利用function和map,直接拿到传入运算符的运算方式,然后传参即可
int evalRPN(vector<string>& tokens) { stack<int> st; map<string, function<int(int, int)>> opFuncMap = { { "+", [](int i, int j){return i + j; } }, { "-", [](int i, int j){return i - j; } }, { "*", [](int i, int j){return i * j; } }, { "/", [](int i, int j){return i / j; } } }; for(auto& str : tokens) { if(opFuncMap.find(str) != opFuncMap.end()) { int right = st.top(); st.pop(); int left = st.top(); st.pop(); st.push(opFuncMap[str](left, right)); } else { // 1、atoi itoa // 2、sprintf scanf // 3、stoi to_string C++11 st.push(stoi(str)); } } return st.top(); }
bind
介绍
用于创建函数对象(可以是前面提到的那三种)的绑定,允许你在调用函数时 预先绑定一些参数或改变参数顺序它提供了一种便捷的方法来部分应用函数、改变参数顺序或创建函数对象 (也就是可以自由控制传参)
std::placeholders
除此之外,bind的使用还需要配合std::placeholders这个作用域中定义的一组占位符对象,它用于指定绑定函数时的参数位置
使用
修改参数位置
也就是说,这里会存在两层调用
- 第一层 -- 新调用对象中的第一个参数传给bind中_1对象的位置
- 第二层 -- 再将该参数传给对应位置的源对象函数形参(也就是这里的a)
- 这里使用的时候并没有调换两参数的位置,但如果我们将bind中_1和_2的位置交换,即可完成参数位置的修改
为参数传固定值
实际上第一种情况我们用的并不多,更多的是自由地对形参传不同的固定值
double PPlus(int a, double rate, int b) { //这里我们将rate设置为固定传参 return (a + b) * 1.0 * rate; } void test4() { function<double(int, int)> new_pplus = bind(PPlus, std::placeholders::_1, 2.3, std::placeholders::_2); new_pplus(1, 1); }
当然,我们可以利用bind生成不同固定传参的变量,而不需要自己去修改函数代码
当我们想要给类的成员函数实现该功能时:
class SubType { public: static int sub(int a, int b) { return a - b; } int ssub(int a, int b, int rate) { return (a - b) * rate; } };
由于bind函数接收一个函数对象+一系列参数,所以需要依靠类名拿到这两种函数的函数指针
- 由于类的静态成员函数可以直接通过类域调用,所以函数指针就是subtype::sub
- 而普通的成员函数必须要拿到地址才行,也就是&subtype::ssub
- 但实际在使用时,都可以在前面+&,这样就不用可以去区分了
- 最重要的一点!!!类的成员函数会将this指针隐式传入,而这个指针不是我们手动传入的,而是通过可调用对象,调用到该函数,然后将该对象的指针作为this指针