【C++11(中)】—— 我与C++的不解之缘(三十一)

发布于:2025-04-05 ⋅ 阅读:(10) ⋅ 点赞:(0)

一、可变参数模版

基本语法:

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...()去计算它的个数,除此之外,唯一能做的就是扩展它了。

那如何扩展呢?

  1. 当扩展一个参数包时,我们还需要提供一个用来扩展每一个元素的模式(扩展包简单来说就是将包中元素一个个取出来),这里对每一个元素应用模式,获得扩展之后的列表。
  2. 通过在模版的右边放一个...来触发扩展操作。
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系列的接口都是可变参数模版,功能上和pushinsert一样;

但是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中新增了两个默认成员函数:移动构造函数和移动赋值重载

  • 如果我们没有自己实现 移动构造函数,且没有实现析构函数拷贝构造拷贝赋值重载中任意一个;此时编译器会生成一个默认移动构造。(默认生成的移动构造,对于内置类型会指向逐成员按字节拷贝;对于自定义类型成员,如果该成员实现了移动构造就调用带成员的移动构造,否则就调用拷贝构造。
  • 如果我们没有自己实现 移动赋值重载函数, 且没有实现析构函数拷贝构造拷贝赋值重载这任意一个,编译器就会生成一个默认移动赋值重载函数。(对于内置类型成员,完成逐字节拷贝;对于自定义类型成员,如果该成员实现了移动赋值重载,就调用移动赋值重载,否则调用拷贝赋值。
  • 如果自己实现了移动构造和移动赋值,编译器就不会生成拷贝构造和拷贝赋值了。

成员声明是给缺省值

成员变量声明时给缺省值,这个缺省值是给初始化列表使用的;

如果没有显示的在初始化列表进行初始化,就会使用这个缺省值去初始化成员变量。

defultdelete

  • C++11设定了defult,让我们更好的控制要使用的默认函数;加入我们要使用某一个编译器生成的默认函数,但是因为我们实现了其他的导致编译器没有生成(我们实现了拷贝构造,编译器就不会生成移动构造);我们可以使用defult关键字来指定要编译器生成。string(string&& str) = defult;(以string为例)。
  • 那如果我们不想要编译器默认生成某一个默认成员函数,在之前,我们可以只声明不定义并设置成私有成员private,这时就不能调用;在C++11中,我们只需要在函数声明后面加上=delete,这样就可以指明让编译器不生成某个默认成员函数。(这里也称=delete修饰的函数为删除函数

finaloverride

final的作用就是禁止类被继承禁止虚函数的重写

override的作用就是:用于显示地标注一个虚函数是对基类虚函数的重写(override)

这里不过多描述了,更多详细内容可以见【继承】—— 我与C++的不解之缘(十九)【多态】—— 我与C++的不解之缘(二十)

STL中容器的一些变化

C++11有了这些新语法以后,STL有更新了一些新的内容:

  • 首先就是每一个容器的emplacepush/insert系列的右值引用版本、移动构造和移动赋值initializer_list这些
  • 其次就是hash版本的unordered_setunordered_map
  • 范围for语法

这些内容我们多多少少都已经了解了,这里就不过多叙述了。

STL还新增了一个容器array

在这里插入图片描述

这个array简单来说就是对数组的封装,这里简单了解一下就OK了。

感谢各位的支持!!!

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws