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;
};