可变参数模板
基本语法及原理
・C++11 支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。例如我们在C语言中使用的printf和scanf,它们都是支持传不同个数和类型的参数
・template <class...Args> void Func (Args... args) {}
・template <class...Args> void Func (Args&... args) {}
・template <class...Args> void Func (Args&&... args) {}
・我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class... 或 typename... 指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟... 指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
・可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
・这里我们可以使用 sizeof... 运算符去计算参数包中参数的个数。
・...表示0到多个参数,所以在使用参数包的时候不传参和传多个参数都是可行的
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print(); // 包⾥有0个参数
Print(1); // 包⾥有1个参数
Print(1, string("xxxxx")); // 包⾥有2个参数
Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
return 0;
}
// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
// 原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能⽀持
// 这⾥的功能,有了可变参数模板,我们进⼀步被解放,他是类型泛化基础
// 上叠加数量变化,让我们泛型编程更灵活。
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
包扩展
・对于一个参数包,我们除了能计算它的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号 (...) 来触发扩展操作。底层的实现细节如图 1 所示。
・C++ 还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。
这里有同学可能会有疑惑,为什么要多写一个showlist()函数,在函数中多加一个判断条件不行吗?
if (sizeof...(args) == 0)
return;
注意:模板递归机制需要处理递归终止的情况,需要有一个接受零个参数的重载版本
template <class T>
const T& GetArg(const T& x)
{
cout << x << " ";
return x;
}
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void Print(Args... args)
{
// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
Arguments(GetArg(args)...);
}
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}
在扩展函数包的时候Arguments(GetArg(args)...);实际上扩展成Arguments(GetArg(x), GetArg(y), GetArg(z));会依次输出1,xxxxx,2.2
empalce系列接口
・template <class... Args> void emplace_back (Args&&... args);
・template <class... Args> iterator emplace (const_iterator position,Args&&... args);
・C++11 以后 STL 容器新增了 empalce 系列的接口,empalce 系列的接口均为模板可变参数,功能上兼容 push 和 insert 系列,但是 empalce 还支持新玩法,假设容器为 container<T>,empalce 还支持直接插入构造 T 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 T 对象。
・emplace_back 总体而言是更高效,推荐以后使用 emplace 系列替代 insert 和 push 系列
・第二个程序中我们模拟实现了 list 的 emplace 和 emplace_back 接口,这里把参数包不断往下传递,最终在结点的构造中直接去匹配容器存储的数据类型 T 的构造,所以达到了前面说的empalce 支持直接插入构造 T 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 T 对象。
・传递参数包过程中,如果是 Args&&... args 的参数包,要用完美转发参数包,方式如下:std::forward<Args>(args)... ,否则编译时包扩展后右值引用变量表达式就变成了左值。
例一:
lt.emplace_back("111111111111");
emplace在传匿名对象的时候达成的效果比push和insert的效果高效多,在传参的时候emplace不会对匿名对象进行拷贝构造,而是直接把构造string参数包往下传,直接用string参数包构造string ,只会产生一个移动构造,而push和insert还会多一个拷贝构造。
但是在传左值和右值的时候并无太大差别。
例二:
lt1.emplace_back("苹果", 1);
在传pair对象的时候,emplace并不用加上大括号,那是因为当容器中存储的元素类型是 std::pair
时,std::pair
有一个构造函数可以接受两个参数,在 lt1.emplace_back("苹果", 1);
这个例子中,编译器会尝试将 "苹果"
和 1
这两个参数与 std::pair
的构造函数进行匹配。std::pair
的构造函数会根据传入参数的类型和顺序来正确地初始化 pair
对象,就好像是直接调用 std::pair<std::string, int>("苹果", 1)
一样。
emplace实现
template <class... Args>
ListNode(Args&&... args)
: _next(nullptr)
, _prev(nullptr)
, _data(std::forward<Args>(args)...)
{}
};
template <class... Args>
void emplace_back(Args&&... args)
{
insert(end(), std::forward<Args>(args)...);
}
template <class... Args>
iterator insert(iterator pos, Args&&... args)
{
Node* cur = pos._node;
Node* newnode = new Node(std::forward<Args>(args)...);
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
这是一份模拟链表的emplace_back,在调用emplace_back的时候,由于参数包的特性,在传匿名对象的时候并不会进行拷贝构造,而是讲参数继续传下去,直到new的时候进行移动构造
新的类功能
默认的移动构造和移动赋值
原来 C++ 类中,有 6 个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 / 取地址重载 /const 取地址重载,最后重要的是前 4 个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上文移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
defult和delete
C++11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显示指定移动构造生成。
如果能想要限制某些默认函数的生成,在 C++98 中,是将该函数设置成 private,并且只声明不定义,这样只要其他人想要调用就会报错。在 C++11 中更简单,只需在该函数声明加上 = delete 即可,该语法指示编译器不生成对应函数的默认版本,称 = delete 修饰的函数为删除函数。有些类不允许被拷贝,如:i/o流,增加deletd关键字就可以不被拷贝
lambda
lambda表达式语法
lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。
lambda 表达式语法使用层面而言没有类型,所以我们一般是用 auto 或者模板参数定义的对象去接收 lambda 对象。
lambda 表达式的格式: [capture-list] (parameters)-> return type {function boby }
[capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表为空也不能省略。
(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同 () 一起省略
->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
例如:加法lambda函数
auto add1 = [](int x, int y)->int {return x + y; }
lanbda不能用来递归,没有函数名也递归不了
只能用当前lambda局部域和捕捉的对象和全局对象
捕捉列表
lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。
第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x,y,&z] 表示 x 和 y 值捕捉,z 引用捕捉。在捕捉列表中,&并不是取地址的符号,而是引用
第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个 = 表示隐式值捕捉,在捕捉列表写一个 & 表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。隐式捕捉实际上不是全部都捕捉,而是使用哪个才捕捉哪个。
第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x] 表示其他变量隐式值捕捉,x 引用捕捉;[&, x, y] 表示其他变量引用捕捉,x 和 y 值捕捉。当使用混合捕捉时,第一个元素必须是 & 或 =,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉,同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉。
lambda 表达式如果在函数局部域中,它可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
默认情况下,lambda 的值捕捉是被 const 修饰的,也就是说传值捕捉的过来的对象不能修改,而传引用捕捉可以被修改,引用捕捉的值在里面被修改后外面的值也会被改变,mutable 加在参数列表的后面可以取消其常量性,也就是说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
lambda的应用
在学习 lambda 表达式之前,我们所使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,lambda 的应用还是很广泛的,以后我们会不断接触到。
lambda的原理
lambda 的原理和范围 for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围 for 这样的东西。范围 for 底层是迭代器,而 lambda 底层是仿函数对象,也就是说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
底层是仿函数 ,捕捉列表相当于成员对象
捕捉列表捕捉到的东西就相当于函数中的成员变量
lambda使用时会调用编译器生成的仿函数operator()
lambda生成对应的仿函数是在编译时,不会影响运行时的效率
仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda 参数 / 返回类型 / 函数体就是仿函数 operator () 的参数 / 返回类型 / 函数体,lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
包装器
function
・std::function 是⼀个类模板,也是⼀个包装器。std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、lambda、bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。
・函数指针、仿函数、lambda 等可调用对象的类型各不相同,std::function 的优势就是统⼀类型,对它们都可以进⾏包装,这样在很多地方就方便声明可调用对象的类型
如:function<int(int,int)> 第一个int是返回值类型,第二个和第三个是参数类型,只要返回值类型和参数类型匹配就可以使用包装器。
包装静态成员函数,成员函数要指定类域并且前面加&才能获取地址,使用function时需要加上this指针,由于包装器来包装成员函数的时候会使用.*去调用this指针,所以才包装成员函数的时候传this指针时传指针,引用,普通类型甚至匿名对象都可以
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
function<int(int, int)> f4 = &Plus::plusi;
Plus pl;
cout << f5(&pl, 1.111, 1.1) << endl;
return 0;
}
bind
bind 是⼀个函数模板,它也是⼀个可调用对象的包装器,可以把它看做⼀个函数适配器,对接收的 fn 可调用对象进行处理后返回⼀个可调用对象。bind 可以⽤来调整参数个数和参数顺序。bind 也在<functional>这个头文件中。
调用 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/_3.... 这些占位符放到 placeholders 的⼀个命名空间中。
#include<functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{
return (a - b) * 10;
}
int SubX(int a, int b, int c)
{
return (a - b - c) * 10;
}
int main()
{
auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl;
auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl;
// 调整参数个数 (常⽤)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;
auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;
// 绑死第1个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;
return 0;
}