【C++】C++11 常见特性

目录

一、列表初始化

[1. { } 初始化](#1. { } 初始化)

[2. std::initializer_list](#2. std::initializer_list)

二、关键字:auto、decltype、nullptr

[1. auto](#1. auto)

[2. decltype](#2. decltype)

[3. nullptr](#3. nullptr)

三、范围for循环

四、认识STL中的变化

五、★右值引用和移动语义★

[1. 左值引用和右值引用](#1. 左值引用和右值引用)

(1)概念

(2)区别

[2. 移动语义(移动构造和移动赋值)](#2. 移动语义(移动构造和移动赋值))

[3. move的作用](#3. move的作用)

[4. 应用场景](#4. 应用场景)

[5. 万能引用](#5. 万能引用)

[6. 完美转发](#6. 完美转发)

六、类的新功能

[1. 默认成员函数](#1. 默认成员函数)

[2. default、delete关键字](#2. default、delete关键字)

七、可变参数模板

[1. 概念](#1. 概念)

[2. STL中的emplace系列](#2. STL中的emplace系列)


一、列表初始化

1. { } 初始化

C++98 中的 { } 只能用于数组C 风格结构体 ;而 C++11 引入了统一初始化,几乎可以用在任何地方,包括普通变量、类对象和 STL 容器。

在C++98中,花括号 { } 被称为聚合初始化 ,有许多限制,只能用于数组,简单的 C 风格结构体 (不能有构造函数、不能有 private 成员、不能有虚函数),STL 容器无法使用,如下所示:

cpp 复制代码
// 数组
int arr1[5] = { 1, 2, 3, 4, 5 }; // 合法:定义数组并初始化
int arr2[] = { 1, 2, 3 };        // 合法:自动推断大小
int arr3[10] = { 0 };            // 合法:将第一个元素设为0,其余自动初始化为0
// int arr[3] {1, 2, 3}; // Error: C++98 不允许省略等号

// C风格结构体(即:不能有构造函数、不能有 private 成员、不能有虚函数)
struct Point { int x, y; };
Point p = { 1, 2 }; // OK

// 容器
// std::vector<int> vec = {1, 2, 3}; // Error: C++98 没有 initializer_list
std::vector<int> vec;
vec.push_back(1); // 只能这样繁琐地添加
vec.push_back(2);

而,在C++11中,就将 { } 提升为统一初始化 ,扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。简单来说就是在C++11中一切都可以通过{ } 进行初始化。而C++11中的变化可以体现在一些几个方面:

基本数据类型也可以使用 { } ,并且可以省略 = 。

cpp 复制代码
// C++11
int a{ 10 };           // 替代 int a = 10;
double d{ 3.14 };      // 替代 double d = 3.14;

// 数组初始化(等号可省略)
int arr[]{ 1, 2, 3, 4, 5 }; // 自动推断大小为 5

// 动态数组初始化
int* pa = new int[3] {1, 2, 3}; // C++11 允许 new 表达式使用 {}

结构体与类对象

没有构造函数的简单结构体(聚合体)

C++11 允许直接按成员声明顺序赋值,无需写构造函数

cpp 复制代码
struct Point 
{
	int x;
	int y;
};

// C++11:直接初始化成员
Point p{ 10, 20 }; // x=10, y=20

// C++98 :必须带等号 =
Point p = { 10, 20 };
// 如果只初始化部分成员,后面的成员会被自动初始化为 0
Point p2 = { 5 }; // p2.x = 5, p2.y = 0

有构造函数的类

创建对象时也可以使用列表初始化方式会调用构造函数初始化,没有构造函数就会报错。

cpp 复制代码
class Student 
{
public:
	// 构造函数
	Student(std::string n, int i) 
		: _name(n)
		, _id(i) 
	{	}
private:
	std::string _name;
	int _id;
};

// C++11:直接使用 { }
Student s1 = { "张三", 20251117 }; // C++98中只有在 Student 是"聚合类"时才合法
Student s2{ "张三", 20251117 }; // 省略等号

// C++98 :使用圆括号 () 或带等号的圆括号
Student s3("Alice", 123);     // 直接初始化
Student s4 = Student("Bob", 456); // 拷贝初始化(调用构造函数创建临时对象,再拷贝构造)

STL容器

允许直接通过 { } 初始化,也可以省略等号。

cpp 复制代码
#include <vector>
#include <map>
#include <string>

// 初始化 vector
std::vector<int> v = { 1, 2, 3, 4, 5 }; 
std::vector<int> v{ 1, 2, 3, 4, 5 }; // 省略等号

// 初始化 map
std::map<std::string, int> mp{
	{"Alice", 95},
	{"Bob", 87},
	{"Charlie", 92}
};

// 初始化嵌套容器
std::vector<std::vector<int>> vv{
	{1, 2, 3},
	{4, 5, 6},
	{7, 8, 9}
};

以上就是C++11变化的地方了,{ }

2. std::initializer_list

std::initializer_list是C++11才增加的一个类型,文档链接:initializer_list - C++ Reference 如图所示:

它用于表示和访问一组特定类型的常量值的数组,主要用于支持使用花括号 { } 进行的列表初始化。而我们使用 { } 初始化的时候编译器都会将这个数据列表转换成一个initializer_list,如以下代码:

使用场景

std::initializer_list一般是作为构造函数的参数,在C++11中,在STL的容器中就增加了许多这样的构造函数,如下所示,这里是vector和map中构造函数的变化。

对于其他STL容器也有一个initializer_list参数的构造函数。也正式因为有了这样的构造函数,所以在我们才可以使用 { } 直接初始化各个容器对象,如:

cpp 复制代码
// 初始化 vector
std::vector<int> v = { 1, 2, 3, 4, 5 };  // 调用了 initializer_list 的构造函数

解释

使用 { } 括起来的初始化列表,在被传递给接受 std::initializer_list<value_type> 的函数或变量时,会被编译器自动转换为一个 std::initializer_list<value_type> 类型的对象,再去调用对应的构造函数。

除了构造时有initializer_list的使用,operator= 也重载了initializer_list参数的使用。

使用示例:

cpp 复制代码
vector<int> v;
v = { 1, 2, 3, 4, 5 }; // 调用了initializer_list参数的operator=

二、关键字:auto、decltype、nullptr

1. auto

在 C++98/03 标准中,auto 用于声明一个具有自动存储期的变量。这意味着变量在进入其作用域时被创建,在离开作用域时被销毁。它的实际用途为几乎为零,因为函数内部定义的局部变量默认就是自动存储期的,所以显式地使用 auto 是多余的。使用示例:

cpp 复制代码
void func() {
    auto int x = 10; // 'auto' 是多余的,x 默认就是自动存储期
    // 等价于 int x = 10;
}

因此,在 C++98 的代码中几乎看不到 auto 。

在C++11中则彻底废弃了auto 在 C++98 中的旧含义,赋予了它新的功能:自动类型推导。即可以自动推到变量类型,在处理复杂类型的时候,就大大方便了我们在C++中的使用。如以下代码:

cpp 复制代码
vector<string> v = { "sort","insert","find"};
// C++98: 类型冗长,可读性差
std::vector<std::string>::iterator it = v.begin();

// C++11: 简洁明了
auto it2 = v.begin(); // 编译器自动推导为 vector<string>::iterator

2. decltype

关键字decltype可以将变量的类型声明为表达式指定的类型。比如,我们有一个int类型的变量,现在我们想定义一个与a相同类型的变量b,要求不使用int关键字,则就可以通过decltype来帮助我们完成定义,如下所示:

cpp 复制代码
int main()
{
	int a = 10;
	decltype(a) b; // b的类型也为 int
	return 0;
}

也适用于表达式,比如,我们要定义一个与一个表达式结果相同的变量,则就可以使用decltype来解决,如下所示:

cpp 复制代码
int main()
{
	int x = 2;
	double y = 3.3;

	decltype(x * y) z; // x*y的结果为double,则z的类型也就为double
	return 0;
}

3. nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示 整形常量。比如函数调用时:

这里我们理解的NULL是空指针,则应该调用void fun(int* a),但是这里却没有调,与想调用指针版本的初衷违背。

所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针,不会有歧义了。


三、范围for循环

C++11中,引入了基于范围的for循环,它适用于对一个有范围的集合进行遍历,比如:我们要遍历一个数组:

cpp 复制代码
int main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	//for (auto e : arr)  // 会自动推导e的类型  -- 方法1
	//for (auto& e : arr) // 传引用就可以直接拿到数组中的值 -- 方法2
	for (int e : arr)  //  使用原本的类型int接收-- 方法3
	{
		cout << e << " ";  // 输出当前元素
	}
	return 0;
}

这里的意思就是:每次循环,arr 的当前元素会被复制到 e 中(值传递),然后循环会依次访问 数组中每一个元素。并且它会自动判断结束。


四、认识STL中的变化

在C++11中的变化有一些几点:

1、增加了几个新容器 ,如图所示(其中框出来的就是C++11新增的容器):

首先我们先看看array,如图所示:

简单来说,array就是一个静态数组,通过T控制类型,N控制数组的大小。其余接口操作也很简单,参考文档链接:array - C++ Reference 即可。其实实际上array并没有怎么使用,因为它和我们的C风格数组差别并不大,唯一提高的就是就是在检查越界上面:在C风格数组中,越界访问时应不会检查,但这样的程序一般会输出垃圾值,或者直接崩溃,甚至出现安全与逻辑错误;而std::array中的是通过operator[ ] 进行访问的,它的内部会进行严格的检查,安全性能更高。

然后就是std::forward_list,它的底层是一个单链表,使用方法和list很类似。

最后就是unordered_map和unordered_set,前面我们已经讲过了,使用方法和set、map类似。

2、增加了新接口

如图所示:

它们其实就是const迭代器和const反向迭代器,实际意义不大,因为这两种迭代器我们在上面的四个函数就已经重载过了,一般我们使用上面四种(即bengin、end、rbegin、rend)就够了。

3. 所有容器都增加了emplace系列

如图所示:

这里的emplace需要我们理解了右值引用可变模板参数才能理解,我们在本文后面再详细解释。

4. 容器增加了移动构造和移动赋值

移动构造和移动赋值可以大大节省我们使用容器的效率。这也需要我们理解了右值引用才能理解,我们在本文后面再详细解释。


五、★右值引用和移动语义★

这是我们学习C++11中的一个难点

1. 左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,而我们 之前使用到的引用叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

(1)概念

首先我们需要来认识一下什么事左值?什么是有右值?

左值 就是一个我们可以对它取地址,一般可以对它赋值 的可以表示数据的表达式,可以出现在赋值符号的左边,比如:

cpp 复制代码
// 左值
int a = 10; // 普通变量
const int b = 20; // const变量
int* p = new int(0);  // 指针变量,p是左值,*p也是左值
"xxxxx"; // 字符串 - 左值,因为它可以取地址:&"xxxxx"合法
auto pp = &"aaaaa"; // 相当于 char (*pp)[6] = &"aaaaa";

右值 则是一个我们不可以对它取地址,也不可以修改 的可以表示数据的表达式,只能出现在赋值符号的右边通常是临时的,生命周期很短。如:

cpp 复制代码
int fmin(int a, int b)
{
	return a < b ? a : b;
}
int main()
{
	double x = 1.1, y = 2.2;

	// 以下都是右值
	10; // 字面量
	x + y; // 表达式结果
	fmin(x, y); // 函数返回非引用类型

    // 这里编译会报错:error C2106: "=": 左操作数必须为左值
    10 = 1;
    x + y = 1;
    fmin(x, y) = 1;
	return 0;
}

区分左值与右值关键在于这个数据可不可以取地址。下面再来认识一下什么是左值引用?什么是右值引用?

左值引用就是对左值去取别名,我们在以前已经学过了,即:左值引用讲解链接 。常见使用如下所示:

cpp 复制代码
int a = 10;
int& ra = a; // 左值引用

而对于右值引用,其实就是对右值取别名,它的写法和左值有些不同,如下所示:

cpp 复制代码
int fmin(int a, int b)
{
	return a < b ? a : b;
}
int main()
{
	double x = 1.1, y = 2.2;
	// 右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	return 0;
}

(2)区别

左值引用:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值(这也是我们为什么一般要在传参数的时候加上const的原因)。

示例:

cpp 复制代码
int main()
{
	// 左值引用只能引用左值
	int a = 10;
	int& ra1 = a;   // ra为a的别名
	//int& ra2 = 10;   // 编译失败,因为10是右值

	// const引用都可以引用
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}

右值引用:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值(move是C++11引入的一个函数,作用是将一个对象转换为右值引用)。
  3. 可以取地址和被修改(在下文"完美转发"会详细解释)

示例:

cpp 复制代码
int main()
{
	int&& r1 = 10;          // 合法:右值引用可以引用右值

	int a = 10; // a 是左值
	// error C2440: "初始化": 无法从"int"转换为"int &&"
	int&& r2 = a; // 编译错误:右值引用不能引用左值(变量 a)

	int&& r3 = std::move(a); // std::move 将左值 a 转换为右值引用
	return 0;
}

2. 移动语义(移动构造和移动赋值)

以下代码是一个简单的string的模拟实现:

cpp 复制代码
namespace MyCreate
{
	class string
	{
	public:
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

首先我们来认识一下移动构造和移动赋值。

移动构造

移动构造就是用一个右值(通常是临时对象) 来构造一个新的对象,即:将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

我们来看下面的这个情况:

在以前没有移动构造的时候(即C++11之前),如下:此时优化前有两次空间的拷贝和销毁,优化后有一次空间的拷贝和销毁。

在C++11中,有了移动构造,则情况就变了,编译器优化前,因为str是左值,所以需要拷贝构造一个临时对象,又因为临时对象是一个右值,所以会移动构造对象s;优化后就是直接移动构造了,因为在C++11中,此时编译器的优化有两点:

  1. 连续的构造,合二为一;
  2. 编译器特殊处理,将这里的返回值str识别成了右值处理(用于调用移动构造)

如图所示:此时因为移动构造,优化前有一次空间的拷贝和销毁,编译器优化后就没有空间拷贝和空间销毁了,因此就没有了拷贝带来的时间消耗了,大大提高了整个程序的效率。

在这里,移动构造的代码如下所示:

cpp 复制代码
// 移动拷贝
string(string&& s) noexcept
	:_str(nullptr)
	, _size(0)
	,_capacity(0)
{
	cout << "string(string&& s) -- 移动拷贝构造" << endl;
	swap(s);
}

移动赋值

移动赋值就是用一个右值 来给一个已经存在的对象 赋值,也就是窃取别人的资源来赋值自己

对于如下情况,编译器也进行了特殊处理,将str识别成了右值(调用移动构造),进行了移动构造了一个临时对象,然后再用临时对象移动赋值对象s。如下所示:

这里并不能将构造和赋值合并。

移动赋值的代码如下所示:

cpp 复制代码
// 移动赋值
string& operator=(string&& s) noexcept
{
	cout << "string& operator=(string&& s) -- 移动赋值重载" << endl;
	swap(s);
	return *this;
}

3. move的作用

move函数的实现比较复杂,但是它的使用是比较简单的,下面我们就来理解一下move的使用功能。它的作用为:

  • 将一个左值转换为右值引用。从而让编译器认为这个对象可以被"移动"(即可以调用移动构造或移动赋值),而不是被"拷贝"

比如有一些代码:

cpp 复制代码
int main()
{
	MyCreate::string s1("xxxxxx");

	std::move(s1); // 单独的使用move并有什么作用
	MyCreate::string copy1 = s1; // s1的资源不会被夺取

	// 可以通过构造或赋值来构造夺取原本数据中的资源
	MyCreate::string copy2 = move(s1); // 构造

	MyCreate::string s2("yyyyyy");
	MyCreate::string copy3;
	copy3 = move(s2); // s2的资源会被夺取

	return 0;
}

4. 应用场景

对于左值引用 的使用场景就是:做函数参数,或则做函数返回值,这样可以减少拷贝,提高效率

比如我们在一个不需要修改参数的函数中如果直接使用值传递,则传参时,就需要拷贝一份,将实参拷贝给形参。对于一些简单的类型(如 int,double 这样的)效率并没有什么差距,但是对于一些复杂类型(比如:vector<vecotr<string>>这样的),使用值传递,拷贝的时间就会比较大,如果使用左值引用,则就不需要拷贝了,这就减少了时间消耗,提高了效率。

对右值引用,它的价值可以总结为:进一步减少拷贝,弥补左值引用没有解决的场景。

右值引用的常见使用场景:

1、函数返回时,对于深拷贝 的类必须传值返回的情况

如果是浅拷贝的类,则不需要实现移动构造,因为浅拷贝的类没有要转移的资源,它的移动跟着拷贝构造的效率没什么差别,所以不用实现。

而对于深拷贝的类,它的资源在堆上,比如如果返回值是string,而这个string又很大,则编译器会将它识别为右值从而调用移动构造来减少拷贝,大大提高效率。

cpp 复制代码
string fun()
{
	string str;
	//...

	return str;
}
int main()
{
	// 情况1
	string ret1 = fun();

	// 情况2
	string ret2;
	ret2 = fun();
}

2、在容器的插入接口中,如果插入的对象是右值,则可以使用移动构造来转移资源给数据结构中的对象,也可以减少拷贝,通过效率。

比如:

5. 万能引用

在模板中的&&代表了万能引用,它既可以引用左值,也可以引用右值。如以下代码:

cpp 复制代码
template<typename T>
void PerfectForward(T&& t)
{
	// ...
}

如果传递的实参是左值,那就是左值引用(有些地方也叫引用折叠)。如:

cpp 复制代码
template<typename T>
void Fun(T&& t)
{
	// 当前t的类型是int&&
	cout << t << endl;
}
int main()
{
	Fun(10); // 右值
	return 0;
}

如果实参是右值,那就是右值引用。如:

cpp 复制代码
template<typename T>
void Fun(T&& t)
{
	// 当前t的类型是int&
	cout << t << endl;
}
int main()
{
	int a = 20;
	Fun(a); // 左值
	return 0;
}

6. 完美转发

之前我们说通过万能引用可以引用左值和右值,实参是左值就是左值引用,实参是右值就是右值引用。看如下代码:

cpp 复制代码
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
	Fun(t); 
}
int main()
{
	PerfectForward(10); // 右值
	int a;
	PerfectForward(a); // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b); // const左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

运行后会发现输出结果都是左值引用或const左值引用,即:

这是因为右值引用变量 的属性都会被编译器识别成左值,因为右值是不能取地址和被修改的,但是右值引用后的右值是可以取地址和修改的。比如以下代码可以通过编译:

cpp 复制代码
int main()
{
	int&& r = 10; // r是右值引用

	r++; //可以修改

	int* p = &r; // 可以取地址
	return 0;
}

而完美转发则可以在传参的过程中保留对象原生类型属性 ,如果是左值引用那就是左值引用,如果是右值引用,那就是右值引用。它必须在模板中使用 ,即必须配合万能引用一起使用,如下所示:


六、类的新功能

1. 默认成员函数

原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

而C++11则新增了两个:移动构造函数和移动赋值运算符重载。注意一下一下三点即可:

  1. 如果你没有自己实现移动构造函数 ,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(即这三个函数都没有实现)。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  2. 如果你没有自己实现移动赋值重载函数 ,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(即这三个函数都没有实现),那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  3. 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

2. default、delete关键字

default 关键字可以显示指定对应的默认成员函数生成。

比如如果你定义了其他构造函数,编译器就不再生成默认构造函数,但是使用 default 则可以也可以让它生成默认成员函数。如:

如果你自己定义了析构函数、拷贝构造函数或拷贝赋值重载中任意一个,就都不会生成移动构造和移动赋值重载了。但是我们可以通过default来显示指定编译器强制生成默认的移动构造或移动赋值重载。如:

delete 关键字则和default找相对,delete可以指示编译器不生成对应函数的默认版本,使用方法和default一样,只需要在该函数声明加上=delete即可。


七、可变参数模板

1. 概念

在C++11以前,我们的模板中类模版和函数模版只能含固定数量的模版参数。而在C++11中增加了可变参数模板的概念,它可以让我们的参数可以传递很多个。

下面就是一个基本可变参数的函数模板:

cpp 复制代码
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{
	// ...
}

如果我们要获取一个参数包的值,这里有两种方法:递归函数方式展开和逗号表达式展开。

递归函数方式展开,这种方法很好懂,就是不断减少参数包中的参数,来逐步获取各个参数。如下所示:

cpp 复制代码
// 递归终止函数
void _ShowList()
{
	cout << endl;
}
// 展开函数
template <class T, class ...Args>
void _ShowList(T value, Args... args)
{
	cout << value << " ";
	_ShowList(args...);
}

// 函数模板
template <class ...Args>
void PrintList(Args... args)
{
	_ShowList(args...);
}

int main()
{
	PrintList();
	PrintList(1);
	PrintList(1,2);
	PrintList(1,2,3);
	PrintList(1, 2, 3, std::string("xxxx")); // 任何类型都可以
	return 0;
}

运行结果:

逗号表达式展开,这种方法比较怪,如下所示:

cpp 复制代码
template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... }; // 逗号表达式展开
	cout << endl;
}
int main()
{
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	return 0;
}

这里来解释一下:

(PrintArg(args), 0)是一个逗号表达式,它会从左向右执行,最终这个逗号表达式的结果为最右边的结果(这里是 0)。(PrintArg(args), 0) 的作用是:先调用 PrintArg 打印参数,然后这个表达式整体的值是 0。

... 是参数包展开的意思,也就是将{ (PrintArg(args), 0)... }展开成{ (PrintArg(args1), 0),PrintArg(args2), 0),PrintArg(args3), 0),PrintArg(args4), 0) ... } 直到参数包全部都展开完。

由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分Printarg(args) 打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包

2. STL中的emplace系列

如图是vector中的接口:

其中emplace对应insert,都是插入的意思,emplace_back对应push_back都是尾插的意思。具体的接口如下所示:

可以看到,它们都是使用可变参数模板 实现的,并且是万能引用,那它们具体有什么作用呢?

我们先来看看下面这段代码:

cpp 复制代码
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date构造" << endl;
	}

	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date拷贝构造" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

template <class ...Args>
Date* Create(Args... args)
{
	Date* ret = new Date(args...); // 使用拷贝模板参数来构造
	return ret;
}

实现了这样的一个有缺省值的构造函数,和一个有可变参数模板的函数模板。这是我们就可以如下这样定义这个函数了:

cpp 复制代码
int main()
{
	// 通过传递参数来构造  
	// 这种方法会通过参数包一直传递到Date的构造函数中从而直接使用传递的参数构造
	Date* d1 = Create();
	Date* d2 = Create(1);
	Date* d3 = Create(1, 2);
	Date* d4 = Create(1, 2, 3);

	// 先构造一个Date对象,此时参数包中就只有一个对象,这是会调用拷贝构造来创建Date对象
	Date* d5 = Create(Date(1, 2));
	Date* d6 = Create(Date(1, 2, 3));
	return 0;
}

运行结果:

总结一下:使用可变参数模板来创建对象就变得很灵活了,既可以传参直接调用构造函数创建,也可以传递对象进行创建。

所以对对emplace系列的接口也可以这样做,既可以通过传递各个参数直接在容器内部构造对象 ,也可传递对象通过移动构造来创建对象。

那么,我们为什么需要 emplace 接口呢?

在 C++11 之前,我们通常使用 push_back 等接口向容器添加对象,但这种方法只能通过传递对象来添加对象,如图所示:

所以如果使用push_back来添加数据。

  1. 就是需要先构造,再(对于没有移动构造的对象/数据)拷贝构造,此时的就需要两次资源的拷贝。
  2. 但是对于具有移动构造的就会先构造,再移动构造,此时拷贝资源也只需要一次,和emplace系列接口相比效率差不多。
  3. 而emplace只需要将参数一直向下传递直接一次构造就可以完成插入了,效率比较高。

而C++11中一般都是移动构造,所以一般来说emplace系列和常规的插入(insert,push_back等)的效率其实都差不多。


感谢各位观看!希望能多多支持!

相关推荐
一切尽在,你来1 小时前
AI 大模型应用开发前置知识:Python 泛型编程全教程
开发语言·人工智能·python·ai编程
shix .1 小时前
旅行网站控制台检测
开发语言·前端·javascript
小付同学呀1 小时前
C语言学习(四)——C语言变量、常量
c语言·开发语言
tankeven2 小时前
HJ92 在字符串中找出连续最长的数字串
c++·算法
艾莉丝努力练剑2 小时前
【Linux:文件】进程间通信
linux·运维·服务器·c语言·网络·c++·人工智能
梦游钓鱼2 小时前
C++指针深度解析:核心概念与工业级实践
开发语言·c++
游乐码2 小时前
c#索引器
开发语言·c#
jaysee-sjc3 小时前
十三、Java入门进阶:异常、泛型、集合与 Stream 流
java·开发语言·算法
Maggie_ssss_supp3 小时前
Linux-python
开发语言·python