C++11(可变参数模板、新的类功能和STL中的一些变化)

C++11(可变参数模板、新的类功能和STL中的一些变化)

  • [1. 可变参数模板](#1. 可变参数模板)
    • [1.1 基本语法及原理](#1.1 基本语法及原理)
    • [1.2 包扩展](#1.2 包扩展)
    • [1.3 emplace系列接口](#1.3 emplace系列接口)
  • [2. 新的类功能](#2. 新的类功能)
    • [2.1 默认的移动构造和移动赋值](#2.1 默认的移动构造和移动赋值)
    • [2.2 成员变量声明时给缺省值](#2.2 成员变量声明时给缺省值)
    • [2.3 default和delete](#2.3 default和delete)
    • [2.4 final和override](#2.4 final和override)
    • [3. STL中的一些变化](#3. STL中的一些变化)

1. 可变参数模板

1.1 基本语法及原理

  1. C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
  2. template <class ...Args> void Func(Args... args) {}
  3. template <class ...Args> void Func(Args&... args) {}
  4. template <class ...Args> void Func(Args&&... args) {}
  5. 我们⽤省略号来指出⼀个模板参数或函数参数表示的⼀个包,在模板参数列表中,class...或typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出接下来表示零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表示,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。
  6. 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
  7. 这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数。
cpp 复制代码
template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl; // sizeof...运算符编译时计算参数包中参数的个数
}

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.2 包扩展

  1. 对于⼀个参数包,我们除了能计算它的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层的实现细节如下图所示。
  2. C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
cpp 复制代码
// 可变模板参数
// 参数类型可变
// 参数个数可变
// 打印参数包内容
template <class ...Args>
void Print(Args... args)
{
	// 可变参数模板编译时解析
	// 下⾯是运⾏获取和解析,所以不⽀持这样⽤
	cout << sizeof...(args) << endl;
	
	//err
	for (size_t i = 0; i < sizeof...(args); i++)
	{
		cout << args[i] << " ";
	}
	cout << endl;
}

要递归推演的包扩展

cpp 复制代码
void ShowList()
{
	// 编译时递归的终止条件
	cout << endl;
}

//err
//template <class T, class ...Args>
//void ShowList(T x, Args... args)
//{
//	cout << x << " ";
//	
//	// 运行时条件判定,所以不能这样写
//	if (sizeof...(args) == 0)
//		return;
//
//	ShowList(args...); // 编译时递归
//}

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 Print(Args... args)
{
	ShowList(args...);
}

int main()
{
	Print();
	Print(1);
	Print(1, string("xxxxx"));
	Print(1, string("xxxxx"), 2.2);
	return 0;
}
cpp 复制代码
void ShowList()
{
	cout << endl;
}

void ShowList(double x)
{
	cout << x << " ";
	ShowList();
}

void ShowList(string x, double x3)
{
	cout << x << " ";
	ShowList(x3);
}

void ShowList(int x, string x2, double x3)
{
	cout << x << " ";
	ShowList(x2, x3);
}

void Print(int x1, string x2, double x3)
{
	ShowList(x1, x2, x3);
}

int main()
{
	Print(1, string("xxxxx"), 2.2);
	return 0;
}

不用递归推演的包扩展

cpp 复制代码
template <class T>
const T& GetArg(const T& x)
{
	cout << x << " ";
	return x;
} 

//template <class T>
//int GetArg(const T& x)
//{
//	cout << x << " ";
//	// GetArg返回什么不重要,只要返回N个参数就行
//  // 因为返回的参数在Arguments中什么也不干
//	return 0;
//}

template <class ...Args>
void Arguments(Args... args)
{}

template <class ...Args>
void Print(Args... args)
{
	// 注意GetArg必须返回获得到的对象,这样才能组成参数包给Arguments
	// GetArg的返回值组成实参参数包,传给Arguments
	Arguments(GetArg(args)...); // 这个不用递归推演,就是包扩展
}


// 本质可以理解为编译器编译时,包的扩展模式将上⾯的函数模板扩展实例化为下⾯的函数
//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}

int main()
{
	Print(1, string("xxxxx"), 2.2);
	return 0;
}

为什么是倒序打印?

函数调用参数求值顺序​​:

在C++标准中,​​函数参数的求值顺序是未指定的​​

大多数编译器的实际实现是​​从右到左​​ 求值

所以实际执行可能是:GetArg(2.2)→ GetArg(string("xxxxx"))→ GetArg(1)

1.3 emplace系列接口

  1. template <class... Args> void emplace_back (Args&&... args);
  2. template <class... Args> iterator emplace (const_iterator position, Args&&... args);
  3. C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container< T >,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
  4. emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
  5. 第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
  6. 传递参数包过程中,如果是Args&&... args 的参数包,要⽤完美转发参数包,⽅式如下std::forward< Args >(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。
cpp 复制代码
// emplace系列总体而言更高效,推荐以后使用emplace系列替代insert和push系列
int main()
{
	std::list<bs::string> lt1;
	// 传左值,跟push_back一样,走拷贝构造
	bs::string s1("1111111111111");
	lt1.push_back(s1);
	lt1.emplace_back(s1);
	cout << "**********************************" << endl;
	// 传右值,跟push_back一样,走移动构造
	bs::string s2("11111111111111111");
	lt1.push_back(move(s2));
	bs::string s3("111111111111111111");
	lt1.emplace_back(move(s3));
	cout << "**********************************" << endl;
	// emplace_back的效率略高一筹
	lt1.push_back("1111111111111111111"); // 单参数构造的隐式类型转换
	// 直接把构造string参数包往下传,直接用string参数包构造string
	lt1.emplace_back("1111111111111111111");
	cout << "**********************************" << endl;
	
	std::list<pair<bs::string, int>> lt2;
	// 传左值,跟push_back一样,走拷贝构造
	pair<bs::string, int> kv1("11111111111", 1);
	lt2.push_back(kv1);
	lt2.emplace_back(kv1);
	cout << "**********************************" << endl;
	// 传右值,跟push_back一样,走移动构造
	pair<bs::string, int> kv2("11111111111", 1);
	lt2.push_back(move(kv2));
	pair<bs::string, int> kv3("11111111111", 1);
	lt2.emplace_back(move(kv3));
	cout << "**********************************" << endl;
	// emplace_back的效率略高一筹
	lt2.push_back({ "1111111111111111111", 1 }); // 多参数构造的隐式类型转换
	// 花括号{},编译器会识别为initializer list,而initializer list的参数类型要相同
	// 所以编译器报错: "initializer list": 不是"_Valty"的有效模板参数
	// lt2.emplace_back({ "1111111111111111111", 1 }); // 不支持
	// 参数包传下去,最后直接构造容器上的对象
	lt2.emplace_back("1111111111111111111", 1);
	cout << "**********************************" << endl;

	return 0;
}
cpp 复制代码
//list.h
#pragma once

//list.h
// 无关接口删除了
namespace bs
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next = nullptr;
		list_node<T>* _prev = nullptr;

		//list_node(const T& val)
		//	:_data(val)
		//	, _next(nullptr)
		//	, _prev(nullptr)
		//{}

		//// insert中的val传到这
		//list_node(T&& val = T())
		//	// 再把val强转成右值属性
		//	// val会去调用bs:string的移动构造
		//	:_data(move(val))
		//	, _next(nullptr)
		//	, _prev(nullptr)
		//{}

		template<class... Args>
		list_node(Args&&... args)
			: _data(forward<Args>(args)...)
			, _next(nullptr)
			, _prev(nullptr)
		{}

		list_node() = default;

		// 这里写万能引用效果不好
		// 得加一个默认构造,因为我们在new Node时,
		// 函数模板的X是泛型,还未实例化
		template<class X>
		list_node(X&& val)
			:_data(forward<X>(val))
			, _next(nullptr)
			, _prev(nullptr)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> Self;
		Node* _node;

		__list_iterator(Node* node)
			:_node(node)
		{
		}

		Ref operator*()
		{
			return _node->_data;
		}

		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		bool operator!=(const Self& it) const
		{
			return _node != it._node;
		}
	};

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}

		//// 左值引用
		//void push_back(const T& x)
		//{
		//	insert(end(), x);
		//}

		//// 右值引用
		//void push_back(T&& x)
		//{
		//	insert(end(), move(x));
		//}

		// 万能引用
		template<class X>
		void push_back(X&& x)
		{
			insert(end(), forward<X>(x));
		}

		template<class... Args>
		void emplace_back(Args&&... args)
		{
			emplace(end(), forward<Args>(args)...);
		}

		template<class... Args>
		iterator emplace(iterator pos, Args&&... args)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(forward<Args>(args)...);

			//prev newnode cur
			prev->_next = newnode;
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;

			++_size;

			//返回新插入节点位置的迭代器
			return iterator(newnode);
		}

		//iterator insert(iterator pos, const T& val)
		//{
		//	Node* cur = pos._node;
		//	Node* prev = cur->_prev;
		//	Node* newnode = new Node(val);
		//	//prev newnode cur
		//	prev->_next = newnode;
		//	newnode->_next = cur;
		//	cur->_prev = newnode;
		//	newnode->_prev = prev;
		//	++_size;
		//	//返回新插入节点位置的迭代器
		//	return iterator(newnode);
		//}

		//// 假设val具有右值属性,那val应该具有常性,不能改变
		//// 但是移动构造/移动赋值需要改变它,所以val必须具有左值属性
		//iterator insert(iterator pos, T&& val)
		//{
		//	Node* cur = pos._node;
		//	Node* prev = cur->_prev;
		//	// 既然val具有左值属性,那么要想调用移动构造
		//	// 要把val强转成右值属性传过去才行
		//	Node* newnode = new Node(move(val));
		//	//prev newnode cur
		//	prev->_next = newnode;
		//	newnode->_next = cur;
		//	cur->_prev = newnode;
		//	newnode->_prev = prev;
		//	++_size;
		//	//返回新插入节点位置的迭代器
		//	return iterator(newnode);
		//}
		
		// X的类型是实参传递给形参推出来的
		// 万能引用
		template<class X>
		iterator insert(iterator pos, X&& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(forward<X>(val));
			//prev newnode cur
			prev->_next = newnode;
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			++_size;
			//返回新插入节点位置的迭代器
			return iterator(newnode);
		}

	private:
		Node* _head;
		size_t _size = 0;
	};
}
cpp 复制代码
// 换成我们自己写的list,方便观察
#include "list.h"

int main()
{
	bs::list<bs::string> lt1; // 我们自己写的有一个哨兵位
	cout << "**********************************" << endl;
	// 传左值,跟push_back一样,走拷贝构造
	bs::string s1("1111111111111");
	lt1.push_back(s1);
	lt1.emplace_back(s1);
	cout << "**********************************" << endl;
	// 传右值,跟push_back一样,走移动构造
	bs::string s2("11111111111111111");
	lt1.push_back(move(s2));
	bs::string s3("111111111111111111");
	lt1.emplace_back(move(s3));
	cout << "**********************************" << endl;
	// emplace_back的效率略高一筹
	lt1.push_back("1111111111111111111"); // 单参数构造的隐式类型转换
	// 直接把构造string参数包往下传,直接用string参数包构造string
	lt1.emplace_back("1111111111111111111");
	cout << "**********************************" << endl;

	bs::list<pair<bs::string, int>> lt2;// 我们自己写的有一个哨兵位
	cout << "**********************************" << endl;
	// 传左值,跟push_back一样,走拷贝构造
	pair<bs::string, int> kv1("11111111111", 1);
	lt2.push_back(kv1);
	lt2.emplace_back(kv1);
	cout << "**********************************" << endl;
	// 传右值,跟push_back一样,走移动构造
	pair<bs::string, int> kv2("11111111111", 1);
	lt2.push_back(move(kv2));
	pair<bs::string, int> kv3("11111111111", 1);
	lt2.emplace_back(move(kv3));
	cout << "**********************************" << endl;
	// emplace_back的效率略高一筹
	// 下面这句代码不能用万能引用,因为花括号初始化列表本身​​没有具体的类型信息​
	// 模板参数 x 无法被推导出来
	// 模板参数推导规则​:​
	// 1. 模板参数推导需要明确的类型信息
	// 2. 但{ ... }在编译期没有确定的类型
	// 3. 编译器不知道 x 应该是什么类型
	//lt2.push_back({ "1111111111111111111", 1 }); // 多参数构造的隐式类型转换
	// 把类型给出来就行了,或者不用万能引用,用一个左值和一个右值引用
	// 编译器底层就是用一个左值和一个右值引用
	lt2.push_back(std::pair<bs::string, int>("11111111111111111111", 1));

	// 花括号{},编译器会识别为initializer list,而initializer list的参数类型要相同
	// 所以编译器报错: "initializer list": 不是"_Valty"的有效模板参数
	// lt2.emplace_back({ "1111111111111111111", 1 }); // 不支持
	// 参数包传下去,最后直接构造容器上的对象
	lt2.emplace_back("1111111111111111111", 1);
	cout << "**********************************" << endl;

	return 0;
}

2. 新的类功能

2.1 默认的移动构造和移动赋值

  1. 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
  2. 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
  3. 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动赋值函数,对于内置类型成员会执⾏逐成员按字节移动,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
  4. 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。

2.2 成员变量声明时给缺省值

成员变量声明时给缺省值是给初始化列表⽤的,如果没有显式在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化

2.3 default和delete

C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显式指定移动构造⽣成。

如果想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。

2.1 / 2.2 / 2.3

cpp 复制代码
class Person
{
public:

	Person(const char* name = "", int age = 0)
		: _name(name)
		, _age(age)
	{}

	// 委托构造
	Person(int i, const char* name = "", int age = 0)
		:Person(name, age)
	{
		_i = i;
	}

	// 强制不让生成,不期望这个类的对象被拷贝
	//Person(const Person& p) = delete;

	//Person(const Person& p)
	//:_name(p._name)
	//,_age(p._age)
	//{}
	
	// 强制生成
	//Person(Person&& p) = default;

	//Person& operator=(const Person& p)
	//{
	//	if(this != &p)
	//	{
	//		_name = p._name;
	//		_age = p._age;
	//	}
	//	return *this;
	//}

	//~Person()
	//{}

private:
	bs::string _name;
	int _age;
	int _i = 0;
};

int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = move(s1);
	Person s4;
	s4 = move(s2);
}

2.4 final和override

我截图了C++多态对final和override的描述,具体可以去C++多态文章里了解。

3. STL中的一些变化

  1. 下图圈起来的就是STL中的新容器,但是实际最有⽤的是unordered_map和unordered_set。这两个我们前⾯已经进⾏了⾮常详细的讲解,其他的⼤家了解⼀下即可。
  2. STL中容器的新接⼝也不少,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列接⼝和移动构造和移动赋值,还有initializer_list版本的构造等,这些前⾯都讲过了,还有⼀些⽆关痛痒的如cbegin/cend等需要时查查⽂档即可。
  3. 容器的范围for遍历,这个在容器部分也讲过了。
相关推荐
奶茶树3 小时前
【C++】12.多态(超详解)
开发语言·c++
草莓熊Lotso3 小时前
《算法闯关指南:优选算法--二分查找》--17.二分查找(附二分查找算法简介),18. 在排序数组中查找元素的第一个和最后一个位置
开发语言·c++·算法
努力努力再努力wz3 小时前
【C++进阶系列】:万字详解特殊类以及设计模式
java·linux·运维·开发语言·数据结构·c++·设计模式
磨十三3 小时前
【C++进阶】从零实现一个支持动态扩容的 Vector 容器(含移动语义与内存管理详解)
开发语言·c++
bkspiderx3 小时前
C++设计模式之行为型模式:策略模式(Strategy)
c++·设计模式·策略模式
泽虞3 小时前
《Qt应用开发》笔记p4
linux·开发语言·数据库·c++·笔记·qt·算法
ajassi20003 小时前
开源 C++ QT QML 开发(十三)多线程
c++·qt·开源
mahuifa3 小时前
C++(Qt)软件调试---binutils工具集详解(39)
linux·c++·软件调试·binutils
Qt程序员3 小时前
Qt C++ 教程:无边框窗体 + 自定义标题栏 + 圆角 + 拖拽拉升 + 阴影
c++·qt·qt编程·qt开发·qt教程·qt界面开发·qt界面