右值
C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&
- 左值是指存储在内存中、有明确存储地址(可取地址)的数据;
- 右值是指可以提供数据值的数据(不可取地址);
可以对表达式取地址(&)就是左值,否则为右值,所有有名字的变量或对象都是左值,而右值是匿名的。
其中右值又分为将亡值和纯右值
- 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
- 将亡值:与右值引用相关的表达式,比如,T&&类型函数的返回值、 std::move 的返回值等。
右值引用
右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
举个栗子理解右值引用的好处:
A a = 临时对象
,创建一个临时对象,然后给 a 进行初始化操作,最后临时对象析构。可以发现这个临时对象存活的时间非常短,假设 a 对象非常庞大,那么这个过程就耗费了大量的系统资源。采用右值引用,可以使临时对象不销毁,延长其的生命周期,然后直接使用它。 这时对象 a 没有拷贝临时对象的数据,而是直接引用这个临时对象。
右值引用只能用右值来进行初始化
int &&val = 111;
性能优化
在C++中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。
右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高C++应用程序的性能。
实例:
#include <iostream>
using namespace std;
class Test{
public:
Test() : m_num(new int(100))
{
cout << "construct" << endl;
}
Test(const Test& a) : m_num(new int(*a.m_num)) // new新空间,深拷贝
{
cout << "copy construct" << endl;
}
// 添加移动构造函数
Test(Test&& a) : m_num(a.m_num) // 将临时对象的指针复制给新创建的对象的指针
// 意味着新创建对象的指针指向了临时对象的内存空间
// 函数参数使用右值引用,可以确保只有右值才能调用移动构造函数,避免了不必要的拷贝操作
{
a.m_num = nullptr; // 然后将临时对象的指针置空,防止内存空间释放两次
cout << "move construct: my name is sunny" << endl;
}
~Test()
{
delete m_num;
cout << "destruct Test class ..." << endl;
}
int* m_num;
};
Test getObj()
{
Test t;
return t;
}
int main()
{
Test t = getObj();
cout << "t.m_num: " << *t.m_num << endl;
return 0;
};
- 如果没有定义移动构造函数,那么
Test t = getObj();
语句,会先通过无参构造函数,构造出一个临时对象,然后再通过拷贝构造函数,赋值给t
。 - 添加了
移动构造函数(参数为右值引用类型)
,这时并不会调用拷贝构造函数进行深拷贝,而是调用移动构造函数,在这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝,提高了性能。 - 在测试程序中
getObj()
的返回值就是一个将亡值,也就是说是一个右值,在进行赋值操作的时候如果=
右边是一个右值,那么移动构造函数就会被调用。移动构造中使用了右值引用,会将临时对象中的堆内存地址的所有权转移给对象t
,这块内存被成功续命,因此在t对象
中还可以继续使用这块内存。
&& 的特性
在C++中,并不是所有情况下 &&
都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为 T &&
,如果是自动类型推导需要指定为 auto &&
,在这两种场景下 &&
被称作未定的引用类型。另外还有一点需要额外注意: const T &&
和 const auto &&
表示一个右值引用,不是未定引用类型。
template<typename T>
void f(T&& param); // 根据传入的参数确定是左值还是右值
void f1(const T&& param); // 只能传入右值
f(10); // 右值引用
int x = 10;
f(x); // 左值引用
f1(x); // error, x是左值
f1(10); // ok, 10是右值
int x = 520, y = 1314;
auto&& v1 = x; // auto&& 表示一个整形的左值引用
auto&& v2 = 250; // auto&& 表示一个整形的右值引用
decltype(x)&& v3 = y; // error,
// decltype(x)&& 等价于 int&& 是一个右值引用不是未定引用类型
// y是一个左值,不能使用左值初始化一个右值引用类型。
在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠。在C++11中引用折叠的规则如下:
- 只有右值才能推导出右值引用,其他(右值引用、左值、左值引用、常量右值引用、常量左值引用)只能推导出 左值引用
move
使用 std::move
方法可以将左值引用转换为右值引用。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的所有权从一个对象转移到另一个对象,只是转移,并没有移动对象,也没有内存拷贝。
forward(完美转发)
右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值。如果需要按照参数原来的类型转发到另一个函数,可以使用C++11提供的 std::forward()
函数,该函数实现的功能称之为完美转发。
std::forward<T>(t);
- 当
T
为左值引用类型时,t
将被转换为T
类型的左值,否则转化为右值
实例:
#include <iostream>
using namespace std;
template<typename T>
void printValue(T& t)
{
cout << "l-value: " << t << endl;
}
template<typename T>
void printValue(T&& t)
{
cout << "r-value: " << t << endl;
}
template<typename T>
void testForward(T && v)
{
printValue(v); // 没有任何处理,右值引用会被当成左值处理,只能打印l-value
printValue(move(v)); // 转化为右值引用,只能打印 r-value
printValue(forward<T>(v));
cout << endl;
}
int main()
{
testForward(520); // 参数右值,传入函数初始化后被推导为右值引用
int num = 1314; // num是左值
testForward(num); // 左值参数,传入函数初始化后被推导为左值引用
testForward(forward<int>(num)); //模板类型为int,得到一个右值参数,传入函数初始化后被推导为右值引用
testForward(forward<int&>(num)); // 模板类型为int&,得到一个左值参数,传入函数初始化后被推导为左值引用
testForward(forward<int&&>(num)); // 模板类型为int&&,得到一个右值参数,传入函数初始化后被推导为右值引用
return 0;
}