【C++】C++11:右值引用、移动语义万能引用

C++11:右值引用、移动语义万能引用

1、左值引用和右值引用

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

2、左值和左值引用

左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名

cpp 复制代码
int main()
{
	// 左值:可以取地址
	// 以下的p、b、c、*p、s、s[0]就是常见的左值
	int* p = new int(0);
	int b = 1;
	const int c = b;
	*p = 10;
	string s("111111");
	s[0] = 'x';

	cout << &c << endl;
	cout << (void*)&s[0] << endl;
	cout << &s << endl;

	// 左值引用给左值取别名
	int& r1 = b;
	int*& r2 = p;
	int& r3 = *p;
	string& r4 = s;
	char& r5 = s[0];
}

注意:左值可以出现在赋值号的右边,但出现在赋值号左边的一定是左值,赋值号右边的可能是左值也可能是右值

3、右值和右值引用

右值也是一个表示数据的表达式,如字面常量、表示式返回值、函数返回值,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址,右值引用就是对右值的引用,给右值取别名

cpp 复制代码
int main()
{
	// 右值:不能取地址
	double x = 1.1, y = 2.2;
	// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
	10;
	x + y;
	fmin(x, y);
	string("11111");  // 无名临时string对象

	// 不能取地址
	//cout << &10 << endl;
	//cout << &(x+y) << endl;
	//cout << &(fmin(x, y)) << endl;
	//cout << &string("11111") << endl;
	cout << &("11111") << endl; //可以取地址
	

	// 右值引用给右值取别名
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("11111");
	
	return 0;
}

补充:右值不能取地址,但是右值取别名后,会使右值被存储到特定位置,且可取到该位置的地址,也就是说:例如不能取字面常量10的地址,但是用rr1引用后,可以对rr1取地址,也可以修改rr1,如果不想让rr1被修改,可以用const修饰,即const int&& rr1

4、左值引用和右值引用的比较

左值引用

  • 左值引用只能引用左值,不能引用右值
  • 但是const左值引用可以引用右值,也可以引用左值
cpp 复制代码
int main()
{
	// 左值:可以取地址
	// 以下的p、b、c、*p、s、s[0]就是常见的左值
	int* p = new int(0);
	int b = 1;
	const int c = b;
	*p = 10;
	string s("111111");
	s[0] = 'x';
	
	// 左值引用给左值取别名
	int& r1 = b;
	int*& r2 = p;
	int& r3 = *p;
	string& r4 = s;
	char& r5 = s[0];
	
	// 左值引用不能直接引用右值,但是const左值引用可以引用右值
	const int& r6 = 10;
	const int& r7 = x+y;
	return 0;
}

右值引用

  • 右值引用只能引用右值,不能引用左值
  • 但是右值引用可以引用move(左值)
cpp 复制代码
int main()
{
	// 右值
	double x = 1.1, y = 2.2;
	10;
	x + y;
	fmin(x, y);
	string("11111");

	// 左值
	int b = 1;
	string s("111111");
	
	// 右值引用给右值取别名
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("11111");

	// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
	int&& rr5 = move(b);
	string&& rr6 = move(s);

	return 0;
}

注意:move函数不会修改传入变量本身,不改变b、s本身左值的属性,只是返回对象的属性是右值

5、左值引用的使用场景

左值引用可以做函数的参数和函数的返回值,避免在函数传参和函数返回时调用拷贝构造函数,可以提高效率

cpp 复制代码
void f(int& x)
{
	std::cout << "左值引用重载 f(" << x << ")\n";
}

void f(const int& x)
{
	std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}

int main()
{
	int i = 1;
	const int ci = 2;
	// 引用接收,减少了拷贝
	f(i); // 调用 f(int&)
	f(ci); // 调用 f(const int&)
	return 0;
}

左值引用的缺点:当函数返回对象是一个局部变量时,出了函数的作用域就被销毁了,不能引用左值返回,只能传值返回,而传值返回会至少有一次或两次拷贝构造

cpp 复制代码
string addStrings(string num1, string num2)
{
	string str; // 函数内局部string对象
	// ... 中间计算,不断往str追加字符
	reverse(str.begin(), str.end());
	return str; // 返回局部左值str
}

调用代码:

cpp 复制代码
string ret;
ret = addStrings("11111", "2222");

第一次拷贝:return str;str 是函数栈上的局部左值,不能直接当作返回值传给外部

第二次拷贝:ret = addStrings(...) ,这里会调用**拷贝赋值运算符,**把临时对象的数据拷贝给外部变量 ret

编译器优化后变成了一次拷贝构造

这里引入右值引用,来解决左值引用的缺点

6、右值引用的使用场景

为了更好的解决传值返回的问题,引出了右值引用的移动构造和移动赋值

6.1 移动构造

移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值

cpp 复制代码
void f(int& x)  //左值引用
{
	std::cout << "左值引用重载 f(" << x << ")\n";
}

void f(int&& x)  //右值引用
{
	std::cout << "右值引用重载 f(" << x << ")\n";
}

int main()
{
	int i = 1;
	f(i); // 调用 f(int&)
	f(3); // 调用 f(int&&)
	f(std::move(i)); // 调用 f(int&&)
}

输出结果:

左值引用重载 f(1)

右值引用重载 f(3)

右值引用重载 f(1)

左值引用和右值引用作为形参的两个f函数构成函数重载

移动构造本质是将参数右值的资源转移过来占为己有,调用拷贝构造函数时,如果参数为右值就会自动匹配到移动构造,使用移动构造不用再做深拷贝

cpp 复制代码
void swap(string& tmp)
{
	std::swap(_str, tmp._str);
	std::swap(_size, tmp._size);
	std::swap(_capacity, tmp._capacity);
}

// 拷贝构造
string(const string& s)
	:_str(nullptr)
{
	cout << "string(const string& s) -- 拷贝构造" << endl;
	reserve(s._capacity);
	for (auto ch : s)
	{
		push_back(ch);
	}
}

// 移动构造
string(string&& s)
{
	cout << "string(string&& s) -- 移动构造" << endl;
	swap(s);
}

测试代码:

cpp 复制代码
int main()
{
	bit::string s1("xxxxx");
	// 拷贝构造
	bit::string s2 = s1;
	
	// 构造+移动构造,优化后直接构造
	bit::string s3 = bit::string("yyyyy");

	// 移动构造
	bit::string s4 = move(s1);
}

输出结果:

string(const string& s) -- 拷贝构造

string(string&& s) -- 移动构造

string(string&& s) -- 移动构造

6.2 移动赋值

移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用

cpp 复制代码
string& operator=(const string& s)
{
	cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
	if (this != &s)
	{
		_str[0] = '\0';
		_size = 0;
		reserve(s._capacity);
		for (auto ch : s)
		{
			push_back(ch);
		}
	}
	return *this;
}

// s4 = bit::string("yyyyy");
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动赋值" << endl;

	swap(s);
	return *this;
}

测试代码:

cpp 复制代码
int main()
{
	bit::string s5("yyyyyyyyyyyyyyyyy");
	s3 = s5;
	s4 = bit::string("yyyyy");
	
	return 0;
}

string& operator=(const string& s) -- 拷贝赋值

string& operator=(string&& s) -- 移动赋值

7、左值引用和右值引用的总结

左值引用用引用的方式减少拷贝,如果待拷贝的对象是一个需要深拷贝的自定义类型,那么拷贝的代价会很大,右值引用解决了左值引用传值返回时需要进行深拷贝的问题,如果被拷贝的对象是一个左值,还是需要调用拷贝构造进行深拷贝

8、List中的右值引用

8.1 List中的push_back

cpp 复制代码
void push_back(const T& x)
{
	insert(end(), x);
}

void push_back(T&& x)
{
	insert(end(), (T&&)x);
}

void insert(iterator pos, const T& x)
void insert(iterator pos, T&& x)

测试代码:

cpp 复制代码
int main()
{
	MyList::list<MyList::string> lt;
	// 左值
	MyList::string s1("111111111111111111111");
	lt.push_back(s1);
	
	//右值
	lt.push_back(move(s1));
	
	return 0;
}

这里push_back一个右值,但是经过调试发现,insert没有调用右值版本的insert函数,因为右值引用本身的属性是左值,因此push_back函数的右值引用是左值,所以会调用非右值版本的insert函数

右值引用本身的属性是左值举例说明:

cpp 复制代码
#include <iostream>
using namespace std;

void func(int& x)    // 接收左值
{
    cout << "左值引用版本 func(int&)" << endl;
}

void func(int&& x)   // 接收右值
{
    cout << "右值引用版本 func(int&&)" << endl;
}

int main()
{
    // 1. 字面量10是纯右值,绑定给右值引用rr
    int&& rr = 10;
		
    // 2. rr是有名字的变量,属于左值!匹配 func(int&)
    func(rr);

    // 3. std::move(rr) 将rr强制转换成右值,匹配 func(int&&)
    func(move(rr));

    return 0;
}

输出结果:

左值引用版本 func(int&)

右值引用版本 func(int&&)

用地址说明:

cpp 复制代码
class String
{
public:
    String() { cout << "构造临时对象\n"; }
    String(const String&) { cout << "拷贝构造\n"; }
    String(String&&) { cout << "移动构造\n"; }
};

void test(String& s)
{
    cout << "接收左值,形参地址:" << &s << "\n";
}
void test(String&& s)
{
    cout << "接收右值,形参地址:" << &s << "\n";
}

int main()
{
    // 临时对象 String() 是纯右值,绑定右值引用rs
    String&& rs = String();
    cout << "右值引用变量 rs 的地址:" << &rs << "\n\n";

    // 1. rs有名字、可取地址 → 左值,调用左值重载
    test(rs);
    // 2. move(rs) 转为无名右值,调用右值重载
    test(move(rs));

    return 0;
}

输出结果:

构造临时对象

右值引用变量 rs 的地址:0x7ffe8d7f9480

接收左值,形参地址:0x7ffe8d7f9480

接收右值,形参地址:0x7ffe8d7f9480

左值和右值地址一样

上面的移动构造中swap函数必须是可以修改的,才能进行交换,如果是右值则不能修改

cpp 复制代码
void swap(string& tmp)
{
	std::swap(_str, tmp._str);
	std::swap(_size, tmp._size);
	std::swap(_capacity, tmp._capacity);
}

// 移动构造
string(string&& s)
{
	cout << "string(string&& s) -- 移动构造" << endl;
	swap(s);
}

所以要用move将右值引用的左值属性转换成右值属性

cpp 复制代码
void push_back(T&& x)
{
	insert(end(), move(x));
}

// 其余函数也需要转换
template<class X>
		void insert(iterator pos, X&& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(move(x));

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

			++_size;
		}

// 构造函数也需要改动
list_node(T&& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(x))
		{}

List中push_back的使用

cpp 复制代码
int main()
{
	bit::list<bit::string> lt;
	// 左值
	bit::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;

	// 右值
	lt.push_back(bit::string("22222222222222222222222222222"));
	cout << "*************************" << endl;

	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;

	lt.push_back(move(s1));
	cout << "*************************" << endl << endl;

	bit::string s2("111111111111111111111");
	set<bit::string> s;
	s.insert(s2);
	s.insert("3333333333333333333333333333");

	return 0;
}

9、完美转发

9.1 引用折叠

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

只有两个右值引用叠加在一起结果才是右值引用,剩下的所有搭配全部折叠成左值引用

eg:

cpp 复制代码
// 由于引用折叠限定,f1实例化以后总是一个左值引用
template<class T>
void f1(T& x)
{}

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

int main()
{
	typedef int& lref;
	//typedef int&& rref;
	using rref = int&&;

	int n = 0;

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

	// 没有折叠->实例化为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;
}

由上面的代码看出,只有两个右值引用叠加在一起才是右值引用

同时像f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用

9.2 万能引用

cpp 复制代码
// 万能引用,既可以实例化出左值引用的版本,也可以实例化右值引用的版本
// 传左值就实例化左值引用的函数,传右值就实例化右值引用的函数
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);

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

	return 0;
}

上面的Function(T&& t)函数在传左值 时:T 会自动带上&T&&经过引用折叠,最终变成左值引用;传右值 时:T 是纯原始类型(不带 &),T&&直接就是右值引用;

注意 :类似void func(int&& t); 不是万能引用,只是普通的右值引用,因为T已经固定,不会推导折叠,只有模板参数T配合&&,才会触发特殊类型推导和引用折叠

比如在List中:

cpp 复制代码
// 不是万能引用
// T是通过实参传递推导的吗?不是,T是list实例化的
void push_back(T&& x)
{
	//insert(end(), move(x));
	insert(end(), (T&&)x);
}

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

9.3 完美转发

在使用上面的Function(T&& t) 函数时,传左值实例化后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数

因为右值引用的变量表达式的属性是左值,也就是说不管这个变量本身是左值引用 int& t,还是右值引用 int&& t,只要它是一个有名字的局部变量(函数形参 t),它自身的表达式属性一定是左值

我们想保持t对象的属性,就需要forward进行完美转发实现

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<class T>
void Function(T&& t)
{
	//Fun(t);
	Fun(forward<T>(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;
	// b是左值,推导出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;
}

输出结果:

右值引用

左值引用

右值引用

const 左值引用

const 右值引用

使用forward后传入的参数保持了原有的属性,符合完美转发的定义

修改LIst:

cpp 复制代码
#pragma once

#include<assert.h>

namespace MyList
{
	template<class T>
	struct list_node
	{
		list_node* _next;
		list_node* _prev;
		T _data;

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

		list_node(T&& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(x))
		{}
	};

	// typedef list_iterator<T, T&, T*> iterator;
	// typedef list_iterator<T, const T&, const T*> const_iterator;
	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;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

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

		Self operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

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

		Self operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

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

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

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

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

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

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

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

			_size = 0;
		}

		list()
		{
			empty_init();
		}

		list(initializer_list<T> il)
		{
			empty_init();
			for (auto& e : il)
			{
				push_back(e);
			}
		}

		// lt2(lt1)
		//list(const list<T>& lt)
		list(list<T>& lt)
		{
			empty_init();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		// lt1 = lt3
		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			auto it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		size_t size()
		{
			return _size;
		}

		// 不是万能引用
		// T是通过实参传递推导的吗?不是,T是list实例化的
		//void push_back(T&& x)
		//{
		//	//insert(end(), move(x));
		//	insert(end(), (T&&)x);
		//}

		// 万能引用
		template<class X>
		void push_back(X&& x)
		{
			// 保持他的属性,往下传递,左值引用就是保持左值属性,右值引用就保持右值属性
			insert(end(), forward<X>(x));
		}

		template<class X>
		void insert(iterator pos, X&& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(forward<X>(x));

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

			++_size;
		}

		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			prev->_next = next;
			next->_prev = prev;

			delete cur;

			--_size;

			//return iterator(next);
			return next;
		}
	private:
		Node* _head;
		size_t _size;
	};

如果文章中有错误或不足,欢迎大家指正交流。

相关推荐
LingzhiPi6 小时前
零知ESP32--RC522NFC考勤打卡系统
c++·单片机·嵌入式硬件
小灰灰搞电子7 小时前
C++ boost::range 详解:基于最新版本的现代范围处理指南
开发语言·c++
王燕龙(大卫)7 小时前
fastdds笔记
网络·c++·笔记
脱胎换骨-军哥8 小时前
C++ 与 Go 生成质量比拼,谁是分布式主力语言
c++·分布式·golang
快乐星空Maker8 小时前
C++【生存游戏】开发:荒岛往事 第三期
开发语言·c++·游戏·编程语言
郝学胜-神的一滴8 小时前
Qt 高级编程 035:无边框窗口阴影以及圆角双特效实现
开发语言·c++·qt·程序人生·用户界面
AA陈超8 小时前
006 T03 — 蓝图操作指南
c++·游戏·架构·ue5·github·虚幻引擎
小保CPP8 小时前
OCR C++ Tesseract从OpenCV中获取图片
c++·人工智能·opencv·ocr·模式识别·光学字符识别
CHANG_THE_WORLD9 小时前
7.C++标准多线程实现 TCP通信
java·c++·tcp/ip