【C++11】initializer_list | 右值引用 | 完美转发

时间:2024-04-10 09:33:37

一切皆可列表{ }初始化

在C++98,允许花括号{ } 对数组、结构体类型初始化。

class Data
{
public:
	Data(int y, int m, int d)
		:_y(y), _m(m), _d(d)
	{}
private:
	int _y;
	int _m;
	int _d;
};

int arr[4]={0,1,2,3};//列表初始化
Data d1={2024,03,21};//列表初始化 

C++11允许通过{ } 初始化内置类型或者用户自定义类型。同时支持省去赋值=符号

class Data
{
public:
	Data(int y, int m, int d)  :_y(y), _m(m), _d(d)
	{}
private:
	int _y;
	int _m;
	int _d;
};

int main()
{
	int a = 1;
	int b = { 1 };//支持列表初始化
	int c{ 1 };   //支持列表初始化,同时省略=

	Data d1(2024, 03, 21);
	Data d2={ 2024, 03, 21 };//支持列表初始化
	Data d3{ 2024, 03, 21 };//支持列表初始化,同时省略=

    Data* p1=new Data[]{{2023,03,21},{2023,03,22},{2023,03,23}};

	return 0;
}

创建d2时,会先调用{2024 ,03,21}列表构造出一个临时对象,再用临时对象拷贝构造d2。

如何证明?对列表的对象取别名,只有加const后才能通过。

编译器一般将 构造+拷贝构造优化成—>直接构造


std::initializer_list

在C++11中,std::initializer_list是一个模板类,它用于表示初始化列表。

当编译器遇到一个使用花括号{}的初始化列表时,它会尝试构造一个std::initializer_list对象,并将该对象传递给接受std::initializer_list参数的函数或构造函数。

initializer_list类中存在俩个指针,begin和end。

initializer_list同时支持迭代器

	initializer_list<int> il{ 1,2,3,4 };
	initializer_list<int>::iterator it = il.begin();
	while (it != il.end())
	{
		cout << *it << " ";
		++it;
	}

在C++11往后的各种容器中,都支持initializer_list构造

	vector<int> v1{ 1,2,3,4 };
	vector<string> v2{"apple","map","cat"};

 

initializer_list去构造vector的原理 

vector(initializer_list<T> it)
{
	reserve(it.size());
	for (auto& e : it)
	{
		push_back(e);
	}
}

initializer_list的数据是放在常量区,不可修改 


auto自动推演

对于较长的类型,写起来比较复杂,就要auto帮助自动推演。

map<string, string> dict = { { "sort", "排序" }, { "insert", "插入" } };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();  //简化代码

decltype

将对象声明为指定的类型;

typeid().name返回一个字符串,字符串的内容是类型。一般用于打印。

	int x = 1;
	double y = 3.14;
	decltype(x * y) ret;
	cout << "typeid(ret).name():" << typeid(ret).name()<< endl;


新容器

array

是一个静态数组,内存是放在栈上的。

array有俩个模板参数,第一个模板参数是类型,第二个是大小

array的设计初心是为了代替数组,因为数组的越界检查不严格,因此array是严格的断言

但是在日常的使用,完全可以用vector代替。

int main()
{
	array<int, 10> a1;   //定义一个可存储10个int类型元素的array容器
	array<double, 5> a2; //定义一个可存储5个double类型元素的array容器
	return 0;
}

forward_list容器

forwar_list是一个单链表

在实际使用,forward_list的运用少于list

  • forward_list只支持头插头删,尾插尾删的复杂度是O(N);
  • forward_list支持inset_after,插入的时间复杂度是O(N);
  • 删除指定元素要找前一个结点。复杂度是O(N);

unordered_map和unordered_set容器

底层是哈希表实现的map和set


右值引用

什么是左值?

数据的表达式,通常出现在等号的左边,是可以被取地址的。

	int a = 1;
	int* b = new int;
	const int c = 2;

	int d = a; //d也是左值

什么是右值?

字母常量,表达式的返回值,函数的返回值,通常出现在等号的右边

是不能被取地址的;

	10;		//常量
	Add(10, 5);//函数返回值
	5 + 1;	//表达式返回值
  • 右值的本质是一块临时变量,没有被存储起来,即将被销毁的。因此无法取到地址。
  • 值得注意的是,函数的返回值如果返回的是一块实际存储的空间,那么就是不是右值。

 左值引用和右值引用

在C++11新特性后,增加了右值引用的玩法。

左值就是给左值取别名,右值就是给右值取别名。本文着重介绍右值。

右值引用的符号  (类型&&)

	10;		//常量
	Add(10, 5);//函数返回值
	5 + 1;	//表达式返回值

	int&& rp1 = 10;
	int&& rp2 = Add(10, 5);
	int&& rp3 = 5 + 1;

注意:
一个右值被取别名后,就被存储到特定的位置上,就能通过别名修改右值的内容;

左值引用可以引用右值吗?

不能。左值是可以被修改的,右值是常量不可被修改。将右值给左值引用,涉及权限的放大。

给左值引用加上const后,就能引用右值。

	int& rp = Add(10, 5);//出错
	const int& rp = Add(10, 5);//正常

右值引用可以引用右值吗?

右值引用只能引用右值。

右值引用可以引用move后的左值。

	10;//右值
	int i = 10;//左值
	int&& r = move(i);

主要原因:

  1. 语义不匹配:右值引用的设计初衷是为了支持移动语义,即从一个临时对象(右值)中“窃取”资源并转移到另一个对象。左值通常具有持久的存储位置,因此将其与移动语义相关联是不合适的。
  2. 避免意外行为:如果允许右值引用引用左值,那么开发者可能会不小心将左值当作右值来处理,从而导致资源被错误地移动而不是复制。这可能导致程序出现难以调试的错误。

move是一种资源转移,对于左值的资源转移,要非常的谨慎! 


右值引用的场景

移动构造函数和移动赋值运算符

就拿模拟实现的to_string函数的来说明

To_string函数体中的str是一个左值,出了函数作用域,就会被销毁。我们把这个即将销毁的值叫做 “将亡值” 。

这一条语句的执行过程是:str先构造出一个临时对象,再用临时对象拷贝构造出s对象(如果考虑优化,编译器会直接用str构造出s对象)

本文默认不考虑编译器的优化

如果str出了作用域就会被销毁,那么他的资源就被释放,反而重新拷贝出一份一模一样的资源,这是浪费效率的事情,如果在此时发生深拷贝,那么效率会更低。

		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}

移动构造是一种转移资源!


移动赋值

移动赋值同样也是一种转移的思想。

		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}

对于将亡值,编译器会优先调用移动拷贝和移动赋值。


右值通过move引用左值

通过move函数后,s1会被当成右值,调用移动构造函数,会将s1的资源转移到s2上,s1的内容会变为空。

总结:将亡值会调用移动构造和移动赋值来实例化对象

左值可以通过move转移资源,来调用移动构造和移动赋值。


万能引用

万能引用是指在模板中,即可以接收左值又可以接收右值。

template<class T>
void Fun(T&& x) //万能引用
{
	cout << x << endl;
}

int main()
{
	Fun(10);//传入右值
	int i = 1;
	Fun(i);//传左值
	return 0;
}

调用模板函数,传入左值和右值都能被Fun函数接收,故针对模板类的&&是万能引用

右值引用的属性是什么?

void Func(int& x)
{
	cout << "左值引用" << endl;
}
void Func(const int& x)
{
	cout << "const 左值引用" << endl;
}
void Func(int&& x)
{
	cout << "右值引用" << endl;
}
void Func(const int&& x)
{
	cout << "const 右值引用" << endl;
}
template<class T>
void PerfectForward(T&& t)
{
	Func(t);
}
int main()
{
	int a = 10;
	PerfectForward(a);       //左值
	PerfectForward(move(a)); //右值

	const int b = 20;
	PerfectForward(b);       //const 左值
	PerfectForward(move(b)); //const 右值

	return 0;
}

左值和右值都会被万能引用接收,而后非const调用的Func函数是左值引用。

const调用的Func函数是右值引用。

说明右值被右值引用接收后的属性是左值!

针对这一点需要注意的,假如模拟实现的vector容器push_back接收到右值,调用inset()函数(同样实现了右值版本),却发现调不动,因为右值被引用后的结果是左值。

要让insert也调用右值,就必须先move再调用insert。


完美转发

完美转发(Perfect Forwarding)是一种技术,它允许函数模板将其参数无损地转发给另一个函数,保持参数的原始值类别(lvalue或rvalue)和类型。这通常用于编写通用包装器或代理函数,例如标准库中的 std::forward 和 std::tie

template<class T>
void PerfectForward(T&& t)
{
	Func(std::forward<T>(t));
}

有了完美转发之后,就不用担心出现移动构造+赋值的情况,就会出现移动构造+移动赋值,大大减小了资源的利用。