C++11(2)

发布于:2025-05-15 ⋅ 阅读:(13) ⋅ 点赞:(0)

简介:这篇文章是继续介绍C++11的一些新语法知识点,也是对C++11(1)的补充和延续

右值引用和移动语义在传参中的提效

下面是list容器insertpush_back接口实现的右值传参的函数重载,当传入左值时,会去拷贝构造一份该左值插入到容器list中。当传入右值时,会去直接转移该右值的资源,再插入到容器list中,因此便起到了右值引用在传参中的提效只不过要特别注意的一点是,插入的值如果是容器的话,那得支持移动构造

在这里插入图片描述
在这里插入图片描述

// 这个string容器是咱自己实现的,不是库里的string。这样才能看清是拷贝构造,还是移动构造
int main()
{
	std::list<xiao::string> lt;
	xiao::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;
	lt.push_back(xiao::string("22222222222222222222222222222"));
	cout << "*************************" << endl;
	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;
	lt.push_back(move(s1));
	cout << "*************************" << endl;
	return 0;
}

在这里插入图片描述

list容器push_back & insert右值版本的模拟实现

这里为了方便更好展现两接口右值版本的模拟实现,需要用到自己实现的list容器,毕竟不只是牵扯到这两个函数这么简单,倒是有点牵一发而动全身的意思下面的list容器只是为了去配合右值版本的模拟实现,并不完整,但够用了

// 没有重载右值版本的list容器
namespace xiao
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;
		ListNode(const T& data = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(data)
		{}
	};
	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;
		ListIterator(Node* node)
			:_node(node)
		{}
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		Ref operator*()
		{
			return _node->_data;
		}
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
	};
	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<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);
		}
		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}
	private:
		Node* _head;
	};
}

这里new出的一个哨兵位结点可以看这个函数empty_init() ,其次为啥这里的析构与上面那副图片的析构不对等?仅仅只是这个list没去写析构函数,这里为了方便阐述右值版本的模拟实现就没写了,但在实际开发中要特别注意,否则会造成严重的资源泄漏

在这里插入图片描述

这里push_back传参是右值的话,就会匹配下面的这个函数,但是之前咱们讲过,这个x它是具有左值属性的,那就不会走下面右值版本的insert,所以需要将这个x进行强转move(x),但当你运行程序后发现还是拷贝构造,是因为insert中的x又退化到了左值,所以需要对这个new Node(x)中的x再进行强转move(x)使它传给下一层是右值

void push_back(T&& x)
{
	insert(end(), x);
	// insert(end(),move(x));
}
iterator insert(iterator pos, T&& x)
{
	Node* cur = pos._node;
	Node* newnode = new Node(x);
	//Node* newnode = new Node(move(x));
	Node* prev = cur->_prev;
	// prev newnode cur
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
	return iterator(newnode);
}

但当你执行程序后,仍然是拷贝构造。这也太多坑了吧。这是因为传入的这个右值,结点没有相应的右值构造,仍然走的是const左值引用构造函数。OK,那我再重载一个右值构造函数,但仍然是拷贝构造函数,因为这个_data(data)中的data有退化到左值了,所以需要move(data)

ListNode(const T& data = T())
	:_next(nullptr)
	, _prev(nullptr)
	, _data(data)
{}
// 这里重载的结点构造函数不能再给缺省值了
// 全缺省的是默认构造,但默认构造函数只能有一个
ListNode(T&& data)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(data)
	// _data(move(data))
{}

这样一套操作下来就完成了右值引用版本的push_back与insert接口的重载。接下来咱用流程图来清晰的展示向下传值的过程

在这里插入图片描述
在这里插入图片描述

类型分类 (了解即可)

  • C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
  • 纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr(1, 2),str1 + str2 传值返回函数调⽤,或者整形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价C++98中的右值。
  • 将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如move(x)、static_cast<X&&>(x)
  • 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值
  • 值类别 - cppreference.comValue categories这两个关于值类型的中⽂和英⽂的官⽅⽂档,有兴趣可以了解细节。

引用折叠

C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef中的类型操作可以构成引用的引用。

template<class T>
void f2(T&& x)
{
	cout << &x << endl;
}

int main()
{
	// 直接对引用的引用会报错
	int&& a = 2;
	//int&&& b = a; 是不允许对引用的引用
	typedef int&& ref; // using ref = int&&;
	ref& b = a;
	cout << &b << endl;
	cout << &a << endl;

	f2(a); // 打印出来的三个地址是相同的
}

通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了⼀个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用一共具有四种组合,没有为什么,这么去规定是 为了契合某些语法

int main()
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;

	lref& r1 = n; // r1 的类型是 int&
	lref&& r2 = n; // r2 的类型是 int&
	rref& r3 = n; // r3 的类型是 int&
	rref&& r4 = 1; // r4 的类型是 int&&

	return 0;
}

通过这段代码可以去验证,左值引用的其它引用(左值引用或右值引用)永远会被折叠成左值引用,右值引用的右值引用才会被折叠成右值引用

// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}

// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)
{}

int main()
{
	// 没有折叠->实例化为void f1(int& x)
	f1<int>(n);
	f1<int>(0); // 报错
	
	// 折叠->实例化为void f1(int& x)
	f1<int&>(n);
	f1<int&>(0); // 报错
	
	// 折叠->实例化为void f1(int& x)
	f1<int&&>(n);
	f1<int&&>(0); // 报错
	
	// 折叠->实例化为void f1(const int& x)
	f1<const int&>(n);
	f1<const int&>(0);

	// 折叠->实例化为void f1(const int& x)
	f1<const int&&>(n);
	f1<const int&&>(0);

	// 没有折叠->实例化为void f2(int&& x)
	f2<int>(n); // 报错
	f2<int>(0);

	// 折叠->实例化为void f2(int& x)
	f2<int&>(n);
	f2<int&>(0); // 报错

	// 折叠->实例化为void f2(int&& x)
	f2<int&&>(n); // 报错
	f2<int&&>(0);


	return 0;
}

万能引用

如果该模板函数的参数是右值引用,即template<class T> void f2(T&& x)这个和之前的那个实例化的函数参数的右值引用是不一样的,之前实例化的函数参数x的类型它是确定的,比如(int/double && x)。而模板函数它的类型是不确定的,它会去推导,而又因为引用折叠的规则,就会导致传递左值的时候函数参数是左值引用,传递右值的时候函数参数是右值引用,因此也把这种函数模板的参数称为万能引用

在这里插入图片描述

这里可以这么去理解T推导出来的类型,传参时Function(value)如果value是左值的话,那Function的形参它得是 int t 或者 int& t,那这个T会被推导成啥类型才能配合T&&引用折叠成出上面两种类型呢?value是右值的话,那Function的形参必须得是int&& t,T可能会被推导出int或者int &&,具体是哪种得看函数内部的实现,下面就有个特殊的例子

template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	x++;
	cout << &a << endl;
	cout << &x << endl << endl;
}
int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(10); // 右值
	int a;

	// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
	Function(a); // 左值

	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(std::move(a)); // 右值
	
	const int b = 8;
	// b是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)
	// 所以Function内部会编译报错,x不能++
	Function(b); // const 左值

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
	// 所以Function内部会编译报错,x不能++
	Function(std::move(b)); // const 右值
	return 0;
}

下面的这段代码可能会存在疑问,为啥这个T推导出来是int而不是int &&呢?T &&,当T为int &&的时候照样会被折叠成右值引用啊(右值引用的右值引用才会被折叠成右值引用)。是没问题的,但载函数中 int a = 0; T x = a;当T为int &&时,变量x无法去作变量a的别名,因为a是一个左值

// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值

完美转发(跟引用折叠有关)

完美转发是什么意思?为啥跟引用折叠有关?需要去解决一个什么样的问题

  1. 完美转发它本质上就是一个函数模板,forward
lvalue (1)	
template <class T> T&& forward (typename remove_reference<T>::type& arg) noexcept;
rvalue (2)	
template <class T> T&& forward (typename remove_reference<T>::type&& arg) noexcept;
  1. 为啥跟引用折叠有关,需要去解决一个什么样的问题
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<class T>
void Function(T&& t)
{
	Fun(t);
}
int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(10); // 右值
	int a;
	// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
	Function(a); // 左值
	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
	Function(std::move(a)); // 右值
	const int b = 8;
	// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)
	Function(b); // const 左值

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
	Function(std::move(b)); // const 右值
	return 0;
}

上面的代码它重载了四个Fun函数,我们在main函数中往Function函数中传入左值或右值,那该模板函数就会发生引用折叠,此时参数t就会产生不同的类型(int&,int&&,const int&等)。那就需要各自类型的参数t去匹配不同的Fun函数,可是当我们去运行代码后,发现咱都是跟左值引用相关的啊!

在这里插入图片描述

这个问题咱也遇到了很多次了,无论这个t的类型是左值引用还是右值引用,左值引用(int& t)它的属性是左值,右值引用(int&& t)它的属性会退化成左值属性,因此都全部走左值引用的Fun函数了。那如果用move(t)强转呢?首先这个t的类型是编译器根据传入的值去引用折叠推导,咱也不知道那个是左值就给它强转成右值啊,你直接move那就全部走右值引用的Fun函数了所以强转也无法解决问题,因此就用到完美转发 forward<T>(t)

你这个t的类型是int&&,t的属性退化成左值,那就给你强转成右值,t的类型是int&,那就给你强转成左值。它的底层本质上还是强转,只不过它会去判断,无需咱们操心

Fun(forward<T>(t));

网站公告

今日签到

点亮在社区的每一天
去签到