一、可变参数
1、可变参数模板
c语言中的 scanf 和 printf 可以支持我们传入任意个数的参数,原理就是用了参数包。
//可变参数包
template<class ...Args>
void Print(Args... args)
{}
Args:模板参数包
args:函数形参参数包
声明一个参数包:Args... args
Args... 表示有0~n个类型,args 表示模板参数定义的形参参数包。
sizeof...(args):求参数包中参数个数
2、参数包在编译时的参数推导递归(方法一)
//最后参数包个数是0时打印换行
void _Printf()
{
cout << endl;
}
//子函数递归调用 把参数包中的值全部打印
template<class T, class ...Args>
void _Printf(T& t, Args... args)
{
cout << t << endl;
_Printf(args...);
}
//主函数调用子函数传入参数包
template<class ...Args>
void Printf(Args... args)
{
_Printf(args...);
}
int main()
{
Printf(1, 2.2, "string");
return 0;
}
结果:
3、运用数组初始化展开参数包(方法二)
template<class T>
int Printf(T t)
{
cout << t << endl;
return 0;
}
template<class ...Args>
void showList(Args... args)
{
//定义数组是为了在构造时展开参数包
int a[] = { Printf(args)... };//函数Printf推导参数包个数
cout << endl;
}
int main()
{
showList(1, "hello", 'x');
return 0;
}
a 数组创建时,会根据 { } 中的参数进行初始化,可以在此直接将可变参数包展开,展开过程中就完成了参数的解析工作。
结果:
4、emplace系列函数
在stl容器中插入数据可以使用emplace系列函数,这样在多数情况下效率最高。
可以看到用的就是参数包形式的传参,在模板中 Args&&... args 是万能引用。
下面我们对比push_back和emplace_back来讨论emplace系列函数的优势
int main()
{
std::list<bit::string> l;
bit::string str1 = "Hello";
bit::string str2 = "Hello";
// 插入左值
l.push_back(str1);
l.emplace_back(str2);
cout << endl;
// 插入 move 出来的右值
l.push_back(move(str1));
l.emplace_back(move(str2));
cout << endl;
return 0;
}
结论1:参数是左值和move得到的右值,两者并无区别。
int main()
{
// 插入纯右值
l.push_back("World");
l.emplace_back("World");
return 0;
}
emplace_back函数直接都没有移动构造,emplace 系列函数可以直接将纯右值作为参数传递,传递途中不展开参数包,直到构造函数才把参数包展开,体现可变参数包 的优势(直接传递参数)
结论2:在插入纯右值,并且构造函数能正常接收时,emplace 系列函数可以直接构造,省去了调用移动构造函数时的开销。
总结
1、对于深拷贝的类型,push_back是构造 + 移动构造,emplace_back是直接构造
2、对于浅拷贝的类型,push_back是构造 + 拷贝构造,emplace_back是直接构造
3、由于push_back函数参数写死,所以只能先构造再拷贝构造,但是emplace_back函数参数是参数包,能够随函数一层一层传递下去,到构造函数时一起构造,省去中间所有构造。
4、在插入单参数,左值,move 之后的右值时两者没有区别,当插入多参数时 emplace 参数包就会随函数调用一层一层向下传,最后直接构造。
类别 | 说明 |
emplace或push / insert 右值对象 | 构造 + 移动构造 |
emplace参数包 | 构造 |
emplace或push / insert 左值对象 | 构造 + 拷贝构造 |
emplace参数包 | 构造 |
int main()
{
list<string> l;
string s("111"); //构造
l.emplace_back(s); //左值深拷贝
l.emplace_back(move(s)); //右值移动构造
l.emplace_back("111"); //单参数参数包最后直接构造
l.emplace_back(string("111")); //构造 + 移动构造
}
二、包装器
1、作用
首先我们先了解可调用对象
可调用对象 | 缺点 |
函数指针 | 类型定义复杂 |
仿函数对象 | 要定义一个类 |
lambda | 没有类型概念 |
C++中的 function 本质是一个类模板,也是一个包装器。为了函数调用方便,统一可调用对象的调用方式,function包装器用来包装对象,由于底层使用的是operator()函数重载,所以调用方式就能得到统一。
Ret:返回值类型
Args:参数包
2、使用包装器
(1)定义包装器
#include<functional>
function<返回类型(参数包类型)> fc;
(2)举例
int f(int a, int b)
{
return a + b;
}
struct add
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
//函数指针包装
function<int(int, int)> fc1 = f;
//仿函数对象包装
function<int(int, int)> fc2 = add();
//lambda表达式包装
function<int(int, int)> fc3 = [](int a, int b) {return a + b; };
return 0;
}
注意:包装器只是包装可调用对象,不是定义可调用对象。
3、理解代码
map<string, functional<int(int, int)> func = opFuncMap = {
{" + ", [](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;}},
};
这是命令与动作的对应实现代码,用到了 initializer_list 初始化4个命令与动作的对应操作。
通过包装器封装的函数最后调用函数的代码就会统一,简单。
function<int(int, int)> func = opFuncMap["+"];
func(1, 2);
4、如何包装静态 / 普通成员函数
class Func
{
public:
static int plusi(int a, int b) {return a + b; };
int plusd(double a, double b) {return a + b; };
}
int main()
{
//包装静态成员函数
function<int(int, int)> f1 = &Func::plusi;
//包装普通成员函数(用对象指针)
function<double(Func*, double, double)> f2 = &Func::plusd;
Func f;
f2(&f, 3.1, 2.1);
//包装普通成员函数(用对象)
function<double(Func, double, double)> f3 = &Func::plusd;
f3(Func(), 3.1, 2.1);
}
(1)获取成员函数函数指针方法:&类型 : : 函数名
(2)普通成员函数参数要包含 this 指针,所以对于普通成员函数的 function 定义就一定要带上 this 指针的参数个数(静态函数因为没有 this 指针不用考虑),这时就有两种方法,一种是定义对象指针,一种是定义对象。
问题:要传的参数是对象的指针,为什么第二种用对象传也可以?
首先 this 指针不能显示传递,所以其实 function 底层就是operator() 用传入的对象调用函数,所以在底层就不关心是不是对象的指针,只关心是不是传对象。
三、绑定
1、作用
调整可调用对象的参数个数或参数顺序。
bind 是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来 “适应” 原对象的参数列表
第二个模板中的 Ret 如果需要修改原来函数对象的返回值,就可以显示实例化来确定特殊的返回类型。
万能引用里面的参数包包括了绑定值和参数(placeholders::_1, placeholders::_2,....)
其中 placeholders::_1 表示传进函数的第一个实参,以此类推。
2、作用一:调整参数顺序
int Sub(int a, int b)
{
return a - b;
}
int main()
{
auto f1 = Sub;
cout << f1(10, 5) << endl;
//调整顺序
auto f2 = bind(Sub, placeholders::_2, placeholders::_1);
cout << f2(10, 5) << endl;
return 0;
}
结果:
原理图:
绑定函数时交换实参的位置就能交换参数。
3、作用二:调整参数个数
原理:绑死几个参数到bind函数里面,调用时参数减少。
class Sub
{
public:
Sub(int x)
:_x(x)
{}
int sub(int a, int b)
{
return a - b;
}
private:
int _x;
};
int main()
{
auto f3 = bind(&Sub::sub, placeholders::_1, placeholders::_2, placeholders::_3);
cout << f3(Sub(1), 10, 5) << endl;
Sub sub(1);
cout << f3(&sub, 10, 5) << endl;
// 绑定,调整参数个数
auto f4 = bind(&Sub::sub, Sub(1), placeholders::_1, placeholders::_2);
cout << f4(10, 5) << endl;
auto f5 = bind(&Sub::sub, &sub, placeholders::_1, placeholders::_2);
cout << f5(10, 5) << endl;
return 0;
}
上面的代码绑定了调用成员函数所用到的对象,这样每次调用函数就不用传对象。
结果: