一、可变参数模版
基本语法:
C++11
支持可变参数模版,简单来说就是支持可变数量参数的函数模版或者类模版;可变数目的参数被称为
参数包
,存在两种参数包:模版参数包
(表示0个或者多个模版参数),函数参数包
(表示0个或者多个函数参数)。
template<class ...Args>
void func(Args ...args)
{}
template<class ...Args>
void func(Args& ...args)
{}
template<class ...Args>
void func(Args&& ...args)
{}
- 我们使用
...
来指出一个模版参数或者函数参数,表示一个参数包; - 在模版参数列表在,
class...
或者typename...
指出接下来的参数表示0
个或者多个
类型列表; - 在函数参数列表中,类型名后跟
...
指出接下来表示0
个或者多个
形参对象列表; - 函数参数包可以使用
左值引用
或右指引用
表示,每一个参数实例化时依然遵循引用折叠的规则。
这里我们可以使用sizeof...()
来计算参数包里面有多少个参数。
template<class ...Args>
void func(Args ...args)
{
cout << sizeof...(args) << endl;
}
int main()
{
func();//0个参数
func(1);//1个参数
func(1, "love");//2个参数
func(1, "love", 1.1);//3个参数
return 0;
}
原理:
这里可变模版参数的原理和模版类似,本质上还是编译器去实例化对应类型和个数的多个函数。
就以上述代码来说,编译器实际上是根据我们传参的类型实例化出来了多个函数:
void func();
void func(int&& a);
void func(int&& a, string&& b);
void func(int&& a, string&& b, double c);
这里我们如果实现下列普通函数模版,也能到达目的:
void func();
template <class T1>
void func(T1&& a);
template <class T1, class T2>
void func(T1&& a, T2&& b);
template <class T1, class T2, class T3>
void func(T1&& a, T2&& b, T3&& c);
但是这样未必有些太麻烦了,如果我们还要传递4、5、6
个甚至更多参数的,那还要一个个去实现。
而有了可变参数模板,我们只需要实现一个,就可以达到普通函数模板多个的效果。
**理解:**这里我们可以简单的理解成,可变参数函数模板先实例化出多个普通函数模板,在这进一步实例化出具体类型的函数。
当然编译器并不会这样去做,而是直接实例化出参数类型个个数对应的函数。
包扩展:
- 对于一个参数包,我们能够使用
sizeof...()
去计算它的个数,除此之外,唯一能做的就是扩展它了。
那如何扩展呢?
- 当扩展一个参数包时,我们还需要提供一个用来扩展每一个元素的模式(扩展包简单来说就是将包中元素一个个取出来),这里对每一个元素应用模式,获得扩展之后的列表。
- 通过在模版的右边放一个
...
来触发扩展操作。
void ShowList()
{
//编译器递归推导,当参数包中参数个数为0时匹配
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
//args参数包中参数的个数为N
//调用ShowList,参数包的第一个参数传给x,剩下的N-1个参数传给第二个参数包
ShowList(args...);
}
template<class ...Args>
void func(Args ...args)
{
ShowList(args);
}
int main()
{
func();//0个参数
func(1);//1个参数
func(1, "love");//2个参数
func(1, "love", 1.1);//3个参数
return 0;
}
这里,也可以这样去扩展包
template <class T>
const T& GetArg(const T& x)
{
cout << x << " ";
return x;
}
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void func(Args... args)
{
// 注意GetArg必须返回获得到的对象,这样才能组成参数包给Arguments
Arguments(GetArg(args)...);
}
这里本质上,编译器将上述模版函数扩展实例化为下面这样
void func(int x, string y, double z)
{
Arguments(GetArg(x), GetArg(y), GetArg(z));
}
包扩展这里简单了解一下就OK了好吧(很少去自己实现或者使用包扩展)
在更多的情况下,是直接将包向下传递,直接匹配参数列表。
二、emplace
系列
template <class... Args>
void emplace_back (Args&&... args);
template <class... Args>
iterator emplace (const_iterator position,Args&&... args);
在C++11
之后,STL
容器新增了emplace
系列的接口,emplace
系列的接口都是可变参数模版,功能上和push
和insert
一样;
但是emplace
也支持一些新的东西,就比如对于容器container<T>
,emplace
还支持直接插入构造T
对象的参数,一些情况下会更加高效(在容器中直接构造T
类型对象,而不是构造临时对象再进行拷贝构造/移动构造
。
int main()
{
list<HL::string> lt1;
HL::string s1("111111111111");
cout << "------------------------------------" << endl;
//左值,调用拷贝构造
lt1.push_back(s1);
cout << "------------------------------------" << endl;
//右值,调用移动构造
lt1.push_back(move(s1));
cout << "------------------------------------" << endl;
//push_back(),先创建临时对象,再调用移动构造
lt1.push_back("111111111111");
cout << "------------------------------------" << endl << endl;
list<HL::string> lt2;
HL::string s2("111111111111");
cout << "------------------------------------" << endl;
//左值,调用拷贝构造
lt2.emplace_back(s2);
cout << "------------------------------------" << endl;
//右值,调用移动构造
lt2.emplace_back(move(s2));
cout << "------------------------------------" << endl;
//emplace_back(),直接构造
lt2.emplace_back("111111111111");
cout << "------------------------------------" << endl << endl;
return 0;
}
这里可以看到,
push_back()
是先构造了临时对象,再进行移动构造;而
emplace_back()
则是直接调用了构造函数。
这里push_back()
是普通函数,在类模版实例化是时候,参数类型就已经确定了,上述代码中是string
;而我们能够使用"111111111111"
作为参数的原因就是:单参数的构造函数支持隐式类型转换。
而emplace_back()
是可变参数函数模版,在类模版实例化时它不会被实例化,所以emplace_back(111111111111")
,它接受的参数类型是const char*
,它就会将参数列表继续向下传递,直到参数类型是const char*
的构造函数。
这里使用上篇文章中自己实现的
string
类(可以去看一下上篇文章的string
类,其中构造函数参数是const char*
(输出了strinf(char* str)-构造
)。
这里了解了emplace
,现在我们来对之前实现过的list
,增加上emplace_back()
和emplace()
接口
这里就只展示新增的代码了,详细请见:【list的模拟实现】—— 我与C++的模拟实现(十四)
ListNode
template <class... Args>
ListNode(Args&&... args)
: _next(nullptr)
, _prev(nullptr)
, _data(std::forward<Args>(args)...)
{}
list
template <class... Args>
void emplace_back(Args&&... args)
{
emplace(end(), std::forward<Args>(args)...);
}
template <class... Args>
iterator emplace(iterator pos, Args&&... args)
{
Node* cur = pos._node;
Node* newnode = new Node(std::forward<Args>(args)...);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
这样我们在调用
emplace_back()
时,它会将参数包继续向下传到emplace
,而emplace
进行构造节点时,将参数包传递给了ListNode
的构造函数。这里要注意,为了保证参数包中参数的右值属性,我们要使用完美转发。
除此之外呢,可变参数函数模版emplace
还可以这样来使用:
int main()
{
list<pair<HL::string, int>> lt1;
pair<HL::string, int> kv("苹果", 1);
//和push_back()一样,对于左值需要进行深拷贝,拷贝到节点中
lt1.emplace_back(kv);
cout << "------------------------------------------------" << endl;
//对于右值就是移动构造
lt1.emplace_back(move(kv));
cout << "------------------------------------------------" << endl;
//push_back进行插入pair类型对象时,必须使用{}括起来(先构造pair类型的临时对象)
lt1.push_back({ "苹果", 1 });
cout << "------------------------------------------------" << endl;
//emplace_back进行插入pair对象时,不能使用{},因为它是可变参数函数模版,编译器不知道{"苹果",1}要构造成什么
//emplace_back()将参数包继续往下传,直到pair类型构造(参数匹配了)
lt1.emplace_back("苹果", 1);
cout << "------------------------------------------------" << endl;
return 0;
}
三、新的类功能
C++11
有了左值
、右值
等等这些概念以后,类有有了一些新的内容
默认的移动构造和移动赋值
在原来的C++
类中,一共有6
个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const取地址重载
;默认成员函数就是我们不写,编译器会生成一个默认的。C++11
中新增了两个默认成员函数:移动构造函数和移动赋值重载。
- 如果我们没有自己实现 移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中任意一个;此时编译器会生成一个默认移动构造。(默认生成的移动构造,对于内置类型会指向逐成员按字节拷贝;对于自定义类型成员,如果该成员实现了移动构造就调用带成员的移动构造,否则就调用拷贝构造。
- 如果我们没有自己实现 移动赋值重载函数, 且没有实现析构函数、拷贝构造、拷贝赋值重载这任意一个,编译器就会生成一个默认移动赋值重载函数。(对于内置类型成员,完成逐字节拷贝;对于自定义类型成员,如果该成员实现了移动赋值重载,就调用移动赋值重载,否则调用拷贝赋值。
- 如果自己实现了移动构造和移动赋值,编译器就不会生成拷贝构造和拷贝赋值了。
成员声明是给缺省值
成员变量声明时给缺省值,这个缺省值是给初始化列表使用的;
如果没有显示的在初始化列表进行初始化,就会使用这个缺省值去初始化成员变量。
defult
和delete
C++11
设定了defult
,让我们更好的控制要使用的默认函数;加入我们要使用某一个编译器生成的默认函数,但是因为我们实现了其他的导致编译器没有生成(我们实现了拷贝构造,编译器就不会生成移动构造);我们可以使用defult
关键字来指定要编译器生成。string(string&& str) = defult;
(以string
为例)。- 那如果我们不想要编译器默认生成某一个默认成员函数,在之前,我们可以只声明不定义并设置成私有成员
private
,这时就不能调用;在C++11
中,我们只需要在函数声明后面加上=delete
,这样就可以指明让编译器不生成某个默认成员函数。(这里也称=delete
修饰的函数为删除函数。
final
和override
final
的作用就是禁止类被继承
和禁止虚函数的重写
。
override
的作用就是:用于显示地标注一个虚函数是对基类虚函数的重写(override)。
这里不过多描述了,更多详细内容可以见【继承】—— 我与C++的不解之缘(十九)和【多态】—— 我与C++的不解之缘(二十)
STL
中容器的一些变化
C++11
有了这些新语法以后,STL
有更新了一些新的内容:
- 首先就是每一个容器的
emplace
、push/insert
系列的右值引用版本、移动构造和移动赋值
、initializer_list
这些- 其次就是
hash
版本的unordered_set
和unordered_map
。- 范围
for
语法这些内容我们多多少少都已经了解了,这里就不过多叙述了。
STL
还新增了一个容器array
这个array
简单来说就是对数组的封装,这里简单了解一下就OK
了。
感谢各位的支持!!!
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws