
🌟🌟作者主页:ephemerals__****
🌟🌟所属专栏:C++
目录
[1. 概念及简单定义](#1. 概念及简单定义)
[2. 包扩展](#2. 包扩展)
[六、 default和delete](#六、 default和delete)
[1. 新容器](#1. 新容器)
[2. 新接口](#2. 新接口)
[1. function](#1. function)
[2. bind](#2. bind)
前言
之前我们学习了c++11的部分新特性:列表初始化、右值引用、类的新默认成员函数和lambda表达式等:
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)-CSDN博客
本篇文章,我们继续学习其余几个:可变参数模板、default和delete、容器新设定、包装器。

正文开始
五、可变参数模板
1. 概念及简单定义
所谓可变参数模板,就是指模板可以接收可变数量的参数。通过处理不同数量的参数类型,代码的灵活性和重用性大大提高。
可变参数模板可以用于类模板,也可用于函数模板。
可变参数模板不仅支持类型实例化,也可支持个数实例化。(参数数量可以变化)
当然,可变参数的函数模板当中,其函数的参数也是可变的。
接下来我们定义一个简易的可变参数模板:
cpp
#include <iostream>
using namespace std;
//可变参数模板
template<class ...Args>
void print(Args... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
print(1);
print(1, 2, 3);
print('a', "xxxx", 3.5, 9);
return 0;
}
这里有几点需要注意:
1. 模板参数定义时,要在前面加上"...",表示这是一个可变模板参数。这里的Args 叫做"模板参数包",它可以接收多个任意类型的参数,在编译时按需实例化。
2. 函数参数的类型使用模板参数包进行定义,其变量名args 叫做"函数参数包",表示多个函数形参。注意函数参数包前面要加上"..."。
**3.**模板参数包和函数参数包可以任意取名,不一定非得是"Args"或者"args"。
4. 函数体当中,我们使用了一个新的运算符"sizeof...",它用于计算函数参数包中的参数个数。
程序运行结果:

这里我们调用了三次print函数,这三次调用使得可变参数模板的形参列表被分别实例化为:
cpp
void print<int>(int args);
void print<int, int, int>(int args, int args, int args);
void print<char, const char *, double, int>(char args, const char *args, double args, int args);
因此,可变参数模板和普通模板的原理还是类似的,本质还是按需实例化,只不过额外支持了参数个数的实例化。
当然,可变参数模板的函数形参也可以用左值引用或右值引用表示,其折叠规则与普通模板相同。
cpp
void print(Args... args)
void print(Args&... args)
void print(Args&&... args)
2. 包扩展
刚才我们定义的可变参数模板当中,只是计算了参数的个数并输出,若只能如此的话,未免也太缺乏实用性了。那么能否对每一个参数进行相应的处理呢? 这里就要使用**"包扩展"**了。
所谓"包扩展",是c++11引入的一个机制,用于分解出函数参数包中的一个个参数。要实现包扩展,就要使用包扩展运算符"..."。
一个简单的使用示例:
cpp
#include <iostream>
using namespace std;
//可变参数模板
template<class ...Args>
void print(Args... args)
{
(cout << ... << args) << endl;
}
int main()
{
print(1, 2, 'a');
return 0;
}
运行结果:

这里我们使用了"..."将args进行扩展,然后一个个地输出。但这种方法的原理较难理解,并且灵活性也较差,所以我们通常用编译时递归实例化的方式进行包扩展。示例如下:
cpp
#include <iostream>
using namespace std;
void ShowList()//递归出口,无参
{
cout << endl;
}
template<class T, class ...Args>
void ShowList(T&& x, Args&&... args)//接收第一个参数和剩余参数包
{
cout << x << ' ';//打印第一个参数
ShowList(forward<Args>(args)...);//将剩余参数包一个个展开,并传入,递归调用
}
template<class ...Args>
void Print(Args&&... args)
{
ShowList(forward<Args>(args)...);//将参数包一个个展开,并传入
}
int main()
{
Print(1, 2, 'a');//函数调用
return 0;
}
运行结果:

不难发现,这里的扩展方法十分巧妙:先使用Print将多个参数打包成函数参数包,然后使用"..."进行扩展,并传入ShowList。ShowList也是一个可变参数模板,它将接收到的参数分解成两部分:第一个参数和剩余部分,并将剩余部分打包成一个新的参数包。函数体内部打印了第一个参数后,将剩余参数包再传给自己去调用 ,这样递归地传入下去,编译器在编译时一个个地按需实例化对应版本,就能够一个一个地输出所有参数了。而当所有参数都被打印后,再次传给自己的参数包就是无参的,此时由于我们定义了一个同名无参函数,当函数模板和同名函数同时符合参数要求时,会优先调用同名函数,这样就相当于递归出口,终止了函数模板的递归调用。

注:这里将参数进行完美转发后传入,是为了保持右值属性,减少递归时的拷贝。
需要注意:传入参数包时,一定要配合包扩展运算符,将参数包分解之后再传入;注意将包括展运算符与参数列表中的"..."区分开,它们不是同一个含义;包扩展运算符要写在参数包之后。
当然,递归式的包扩展也可以这样实现,原理是一样的:
cpp
template <class T>
void Print(T&& x)
{
cout << x << ' ';
}
template <class T, class ... Args>
void Print(T&& x, Args&&... args)
{
cout << x << ' ';
Print(forward<Args>(args)...);
}
六、 default和delete
c++11引入了default和delete这两个关键字(用于类的定义中),它们的作用是对类的默认成员函数进行显式的控制。
假如你要使用某个默认生成的成员函数,但由于一些原因,并没有默认生成,此时你就可以使用default强制生成对应默认成员函数。使用示例:
cpp
class A
{
public:
A(const A& a)//只显式实现了拷贝构造,因此不会生成默认的移动构造和构造函数
{
//...
}
A(A&& a) = default;//强制生成移动构造
A() = default;//强制生成默认构造函数
private:
//...
};
如果你想限制某些默认成员函数,使其不可被外界显式调用,可以将其设置为私有成员,也可以使用delete关键字,将其强制删除。示例:
cpp
class A
{
public:
A& operator=(const A& a) = delete;//强制删除赋值重载
private:
//...
};
注:强制删除某个默认成员函数后,该成员函数也不可显式实现。
七、容器新设定
1. 新容器
c++11引入了几个新的STL容器,例如array(定长数组) 、forward_list(单链表) 、unordered系列容器。其中最为实用的还是unordered系列容器,可以将哈希的高效性充分融入到实际开发中。

c++11 STL容器介绍与使用方法查阅:Reference - C++ Reference
2. 新接口
由于右值引用 和initializer_list的出现,c++11还引入了容器插入、拷贝、初始化相关的全新重载版本,提高了效率。


emplace系列接口
c++11还引入了容器的emplace系列接口,相比push_back、insert等接口,它的特点是直接在容器内部构造元素,而不是而不是先创建临时对象再拷贝或移动到容器中,更加高效。

举个例子:
cpp
#include <iostream>
#include <list>
#include <string>
using namespace std;
int main()
{
list<string> l;
l.push_back("hello world");
l.emplace_back("hello world");
return 0;
}
这里我们分别调用push_back、emplace_back给list尾插两个字符串,相比push_back,emplace_back会更加高效。因为调用push_back时,首先会将"hello world"构造为string类型得临时对象,然后再执行插入操作,最后析构这个string的临时对象;而emplace_back是一个可变参数模板 ,传入"hello world"之后,被实例化为const str*类型,之后直接用字符串构造出一个节点,避免产生string的临时对象,效率更高。
并且,由于其是可变参数模板,所以也支持多个参数传入,并构造节点:
cpp
list<pair<string, int>> l;
l.push_back({ "苹果",1 });
l.emplace_back("苹果", 1); // 多参数传入
当然,许多容器除了emplace_back ,还有emplace_front 、emplace接口,分别支持头插和任意位置的插入操作。
八、函数包装器
函数包装器是c++11引入的工具,它可以对函数进行"封装",后续需要时调用,功能类似于函数指针 ,且更加安全。c++11的函数包装器一共有两个:function和bind。
1. function
function本质是一个类模板 ,包含在头文件**<functional>** 中,它可以接收一个可调用对象,例如函数指针、lambda表达式、仿函数对象等,并可以通过function调用它。这样就可以将不同类型的可调用对象统一化。示例:
cpp
#include <iostream>
#include <functional>
using namespace std;
//函数
int f1(int a, int b)
{
return a + b;
}
//仿函数
struct F2
{
int operator()(int a, int b)
{
return a - b;
}
}f2;
//lambda表达式
auto f3 = [](int a, int b)->int{ return a * b; };
int main()
{
//定义包装器,其接收的可调用对象,参数为两个int,返回值为int
function<int(int, int)> f;
//使用包装器调用可调用对象
f = f1; // 接收函数
cout << f(3, 5) << endl;
f = f2; // 接收仿函数对象
cout << f(3, 5) << endl;
f = f3; // 接收lambda表达式
cout << f(3, 5) << endl;
return 0;
}
运行结果:

定义function时,注意要显式实例化要接收对象的参数和返回值;调用时,直接用函数调用的方式进行传参。
注意:用function包装非静态成员函数时,别忘记第一个参数是this指针,显式实例化时要将对象指针类型写在参数列表中:
cpp
#include <iostream>
#include <functional>
using namespace std;
class A
{
public:
A(int n, int m)
:_n(n)
, _m(m)
{}
void Print()
{
cout << _n + _m << endl;
}
private:
int _n;
int _m;
};
int main()
{
A a(1, 2);
function<void(A*)> f1 = &A::Print; // 包装成员函数Print
f1(&a); // 调用时传入对象地址
return 0;
}
当然,参数也可以不设置为对象指针,可以直接设置成对象本身或对象的引用类型,传参时传入对象即可:
cpp
function<void(A)> f2 = &A::Print;
f2(a);
function<void(A&)> f3 = &A::Print;
f3(a);
function<void(A&&)> f4 = &A::Print;
f4(move(a));
2. bind
bind的本质是一个函数模板 , 包含在头文件**<functional>** 中,它也可以接收可调用对象,但与function不同的是,它可以返回一个在原对象基础上调整传参顺序或增加默认参数的可调用对象,更加灵活。
它的基本使用方式如下:
cpp
#include <iostream>
#include <functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
void fun(int a, int b, int c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
auto f = bind(fun, _1, _2, _3); // 包装fun函数
//函数调用
f(1, 2, 3);
return 0;
}
运行结果:

使用bind包装fun函数时,bind的参数列表第一个位置是fun函数的地址;其余位置表示要传入的参数的代号(注意这个代号在std的placeholders空间中,这里为了方便直接展开了)。然后用一个auto变量接收bind的返回值,就可以对fun函数进行调用。
改变bind的代号的顺序,就可以调整传参顺序:
cpp
int main()
{
auto f = bind(fun, _1, _3, _2); // 调整第二个参数和第三个参数的相对位置
//函数调用
f(10, 20, 30);
return 0;
}
运行结果:

这里我们在包装fun时,交换了第三个参数和第二个参数的相对位置,调用fun时的参数就会按照包装时的顺序进行传入,而不是从左到右的顺序,这样可以使一些原型不一致的函数进行适配。
在bind代号位置传入初值,就可以为任意参数设置初值:
cpp
int main()
{
auto f = bind(fun, _1, 5, _2); // 给第二个参数设置初值5,注意这里的_2不是表示函数的第二个参数,而是调用时传入的第二个参数
//函数调用
f(10, 20);
return 0;
}
运行结果:

相比缺省参数,它的好处是可以给任意位置的参数设置初值,而缺省参数只能从右到左设初值。
有了bind之后,我们就可以绑死一些参数,例如成员函数的对象,这样每次调用成员函数就不用显式写明对象了。
总结
本篇文章我们学习了c++11的四个新语法:可变参数模板、default和delete、容器的新设定和函数包装器。它们大大提高了c++编程的灵活性,也提高了我们的开发效率。之后博主会和大家分享智能指针以及其他包装器的相关知识和用法。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤