目录
一.右值引用
(一).何为右值
(二).右值引用
(三).右值和左值的互相传递
①左值->右值引用
②右值->左值引用
(四).右值引用的自身属性
二.移动构造和移动赋值
(一).移动构造
(二).移动赋值
三.转发
(一).万能引用
(二).完美转发
四.move和forward底层实现方式
(一).move底层实现
(二).forward底层实现
一.右值引用
(一).何为右值
不能取地址的就是右值。例如:字面常量、临时变量。
//1就是右值
int i = 1;
//max返回值是临时变量,也就是右值
int n = max(1, 2);
(二).右值引用
左值引用是对左值的引用,顾名思义,右值引用就是对右值的引用。
右值引用符号为&&,使用方式与左值引用相同,但符号和引用对象属性不同。
左值引用 | 右值引用 | |
符号 | & | && |
引用对象 | 左值(可取地址的变量) | 右值(不可取地址) |
使用方式 | int j = 1; int& a = j; | int&& a = 1; |
(三).右值和左值的互相传递
左值和右值无法直接传递。
int main()
{
int a = 1;
int& b = a;//正确,左值引用
int&& c = b;//错误。右值引用不能传左值
int& d = 1;//错误。左值引用不能传右值
return 0;
}
①左值->右值引用
该函数参数为左值,返回值为右值。
作用是将左值参数强制转化成右值引用。
int a = 1;
int&& c = move(a);//将a强转成右值
②右值->左值引用
左值引用加上const修饰符即可。
const int& d = 1;
(四).右值引用的自身属性
右值引用本身是左值属性。
因此,如果给左值引用传递右值引用是可以的。
int&& a = 1;//a为右值引用,但自身属性是左值
int& b = a;//正确,给左值引用传递左值
这该怎么理解呢?
《C++ Primer》对此给出了相关解释,大意如下:
左值是“持久”的,右值是“短暂”的。即左值只要不出作用域就可以一直存在,但右值只能在使用时的瞬间“存活”(参考函数返回值)。
因此,当进行右值引用后,引用本身可以一直在作用域中存在,那么它就是左值。
当然,可以使用另一种方式证明:取地址。
左值可以取地址,右值不可以取地址。
int main()
{
int a = 1;//a可以取地址,是左值
int&& b = 3;
cout << "a地址: " << &a << endl;
cout << "b地址: " << &b << endl;
return 0;
}
不妨总结一下:
左值 | 右值 | 左值引用 | 右值引用 | |
---|---|---|---|---|
举例 | int a = 1; | string str = "abc"; | int& b = a; | int&& c = 1; |
属性 | 左值 | 右值 | 左值 | 左值 |
取地址 | 能 | 不能 | 能 | 能 |
转化 | 接收右值: 直接传 | 接收左值: 无 | 接收右值: +const | 接收左值: move函数 |
二.移动构造和移动赋值
C++11引入右值引用后,很大的作用便是移动构造与赋值。
比如官方STL库中就提供了相关函数:
(一).移动构造
移动构造的目的在于减少因为参数是左值时引发的重复拷贝的问题。
以string为例进行说明:
截取如下代码:
class String {
...
explicit String(const char* a = "")//默认构造
{
_size = strlen(a);
_capacity = _size;
_a = new char[_capacity + 1];
strcpy(_a, a);
cout << "构造函数\n";
}
String(const String& st)//拷贝构造
:_a(nullptr)
{
String tmp(st.c_str());//调用构造函数
swap(tmp);
cout << "拷贝构造\n";
}
...
};
String To_string(int value)//将int转为string
{
...
String str;
...
return str;
}
int main()
{
String str = To_string(20);
return 0;
}
当我们执行这个程序时,会调用2个默认构造和1个拷贝构造:
分别是to_string内部生成str时调用默认构造、返回临时变量时调用string拷贝构造,但是string拷贝构造内部又会先调用默认构造。
其实这还是优化后,如果没有编译器优化,main函数中str也会再调一次string拷贝构造。
而这一切的“罪魁祸首”是什么呢?——to_string的返回值。
是的,因为to_string内部会生成一个string对象,而该对象是局部变量,出了函数作用域就销毁,因此只能调用拷贝构造to_string内部的对象。
这还只是string类型拷贝构造,如果是更加复杂的类型,拷贝构造往往会造成更多资源的占用。
正因如此,移动构造派上了用场:
String(String&& st)//移动构造函数,但是参数为右值
:_a(nullptr)
{
swap(st);
cout << "string移动构造\n";
}
移动构造的参数为右值,所以当to_string返回str时,会被移动构造接收。
虽然str本身为左值属性,但是因为此时str是“将亡值”,即出了函数作用域就会被销毁,编译器会将这种“即将死亡”的值识别为右值。
在移动构造内部,会将右值的数据与自身数据进行交换。因为右值作为“暂时存在的数据”,把数据交给目标对象,目标对象把“舍弃”的数据交给右值,正好可以“延续”目标数据且消除原本数据。
这时,接收to_string返回值时只需要一个一个移动构造即可:
(二).移动赋值
移动赋值的目的与移动构造类似,在于减少因为赋值造成重复拷贝的问题。
以string为例,其中赋值重载通过调用了拷贝构造函数实现。
class String {
...
String& operator=(const String& st)//赋值重载1
{
String tmp(st);//调用拷贝构造
swap(tmp);
cout << "string赋值\n";
return *this;
}
String& operator=(const char* str)//赋值重载2
{
String tmp(str);
swap(tmp);
cout << "char*赋值\n";
return *this;
}
...
};
int main()
{
String str;
cout << "--------------------------------\n";
str = To_string(1);
return 0;
}
当执行这个程序时,会有多个构造、拷贝构造被调用:
而这其中,属于因为赋值重载而调用的就有三个。
因为赋值重载的参数是左值引用,不能像右值引用那样交换数据,只能调用拷贝构造获取数据。
由此,移动赋值应运而生:
与移动构造相同,移动赋值也是直接与右值交换数据。
String& operator=(String&& st)
{
swap(st);
cout << "string移动赋值\n";
return *this;
}
此时,只需要将to_string的返回值作为右值传给移动赋值即可。
三.转发
(一).万能引用
首先,万能引用只存在与模板编程中。
万能引用就是引用形参既可接收左值也可接收右值,其符号与右值引用相同,但必须是模板。
即当模板的参数是右值引用的形式,如果实参是左值就是左值引用,右值就是右值引用。
例如下列代码:
void Print(int& a)
{
cout << "左值" << endl;
}
void Print(int&& a)
{
cout << "右值" << endl;
}
template<class T>
void func(T&& t)//万能引用
{
Print(t);
}
int main()
{
int a = 0;
func(a);//传左值
func(1);//传右值
return 0;
}
(二).完美转发
上述代码有一个问题,尽管func(1)传入的是右值,但是因为右值引用本身是左值,当调用Print函数时,会调用左值版本,这不符合我们的预期,因为明明传入的是右值:
这时,就需要使用完美转发forward,它会保持传入实参的属性不变:
void func(T&& t)
{
Print(std::forward<T>(t));
}
四.move和forward底层实现方式
(一).move底层实现
首先看一下move函数底层代码:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_case<typename remove_reference<T>::type&&>(t);
}
其中参数T&&是万能引用,可接收左值或右值。
返回值很特殊,typename remove_reference<T>::type的含义就是去掉T的引用类型。
remove_reference本身是模板类,它的作用就是返回一个类型,所以这个类里面只有成员类型。
通过remove_reference源码可以看到,不管传入的是左值引用还是右值引用,它都只会返回这个值去掉引用后的类型。
我们以int为例,不管传入int&还是int&&,经过remove_reference<T>后,返回的都是int。
template <typename T>
struct remove_reference{
typedef T type; //成员类型
};
template <typename T>
struct remove_reference<T&> //左值引用
{
typedef T type;//返回T本身的类型
}
template <typename T>
struct remove_reference<T&&> //右值引用
{
typedef T type;//返回T本身的类型
}
static_case作用是强制类型转换,可以将左值强转成右值,move中是强转成右值引用。
因此,move底层代码可以翻译成如下形式:
template <typename T>
int&& move(T&& t)
{
return (int&&)(t);
}
于是,我们清楚的发现:move函数就是通过remove_reference获取引用对象本身的类型,强转成右值引用的方式实现的。
(二).forward底层实现
这是forward底层代码:
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)//左值引用
{
return static_cast<T&&>(param);//万能引用
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)//右值引用
{
return static_cast<T&&>(param);//万能引用
}
有了move的基础,forward就不难理解了。
它通过remove_reference来区分传入的参数是左值引用还是右值引用,然后调用具体的重载forward函数。
再通过万能引用的形式,根据param的具体类型返回左值引用还是右值引用。
源码可以翻译成如下形式(int为例):
template <typename T>
T&& forward(int& param)//左值引用
{
return (T&&)(param);//万能引用
}
template <typename T>
T&& forward(int&& param)//右值引用
{
return (T&&)(param);//万能引用
}
参考文章:
聊聊C++中的完美转发 - 知乎 (zhihu.com)
C++高阶知识:深入分析移动构造函数及其原理 | 音视跳动科技 (avdancedu.com)
参考书籍:
《C++ Primer》
程序是我的生命,但我相信爱她甚过爱我的生命。——未名
如有错误,敬请斧正