1. 右值引用
1.1左值与右值
C++11中增加了一个新类型,称为右值引用(R-value reference),标记为 &&。在介绍右值引用之前要先了解一下什么是左值和右值,它们的区别是什么:
左值:存储在内存中,有明确存储地址的数据,可以取地址。
右值:提供数据值的数据,不可取地址。
区分左值和右值的方法:看表达式是否可以取地址(&),可以就是左值,否则就是右值。左值在等号左边,具备名字;右值只能在等号右边,不具备名字。
左值:变量名,返回左值引用的函数调用,前置自增/自减,赋值运算/复合赋值运算等。
c++11中右值分为两种:一种叫将亡值(expiring value),一种叫纯右值(PureRvalue)。
将亡值:与右值引用相关的值类型,std::move返回值,T&&类型函数返回值等。
纯右值:字面值,返回非引用类型的函数调用,后置自增/自减,比较/算数/逻辑表达式等。
1.2右值引用
因为右值不具备名字,所以只能通过引用的方式找到它。使用引用类型时要先进行初始化,因为引用类型本身不拥有绑定对象的内存,只是该对象的一个别名,所以通过右值引用的声明,就可获得该右值。它的生命周期与右值引用类型的变量的生命周期相同,该变量活着,与其绑定的右值临时量就会一直活下去。
#include <iostream>
class Test {
public:
Test() {
std::cout << "construct" << std::endl;
}
Test(const Test& a) {
std::cout << "copy construct" << std::endl;
}
};
Test getObj() {
return Test();
}
int main()
{
int&& value = 100;
int a1;
int &&a2 = a1; // error
Test& t = getObj(); // error
Test && t = getObj();
const Test& t = getObj();
return 0;
}
在上面的代码中:
-
int&& value = 100; 的100是纯右值,value是对其的引用。
-
在int &&a2 = a1;中a1虽然在等号右边,但是它仍然是一个左值,不能使用左值去初始化一个右值引用类型。
-
在Test& t = getObj();这句代码中,右值不能赋值给一个左值引用类型。
-
在Test&& t = getObj();中getObj()返回的临时对象是一个将亡值,t为该将亡值的右值引用。
-
在const Test& t = getObj();中const Test& 为常量左值引用类型,是一个万能引用类型,可以接受左值、右值、常量左值、常量右值。
1.3左值引用和右值引用的区别
左值引用:避免对象的拷贝,比如函数传参,函数返回值等
右值引用:实现移动语义;实现完美转发;通过std::move()可以指向左值
声明出来的左值引用和右值引用都是左值
由于C++在进行对象赋值操作时,经常进行对象间的深拷贝,当占用的堆内存很大时,这种拷贝的代价也非常大。使用右值引用就可以避免对象的深拷贝,提升性能。
1.3.1 移动语义
移动语义可以在对象赋值时避免资源的重新分配,即通过浅拷贝将资源从一个对象转移到另一个对象,减少不必要的临时对象的创建、拷贝、销毁,可以大幅提高C++应用程序的性能。
int a = 5; // a是个左值
int &ref_left = a; // 左值引用指向左值
int &&ref_right = std::move(a); //std::move将左值转化为右值,可以被右值引用指向
cout << a; // 打印结果:5
在上面代码中,move()函数并没有移动内容,而是将类型进行转换,将左值转换成右值,让右值引用指向它,里面的数据并没发生改变。等同于类型转换:static_cast<T&&>(lvalue)。
所以右值引用能够指向右值,本质就是将右值变成左值,定义一个右值引用,并通过std::move()指向该左值。
int &&ref_val = 5;
等价于:
int temp = 5;
int &&ref_val = std::move(temp);
在移动构造中使用右值引用会将临时对象的堆内存地址的所有权转移给返回的对象t,所以在t对象中还可以继续使用这块内存。
#include <iostream>
class Test {
public:
Test() : m_val(new int(10)) {
std::cout << "construct" << std::endl;
}
Test(const Test& a) : m_val(new int(*a.m_val)) { //拷贝构造函数
std::cout << "copy construct" << std::endl;
}
Test(Test&& a) noexcept : m_val(a.m_val) { //移动构造函数
a.m_val = nullptr;
std::cout << "move construct" << std::endl;
}
~Test() {
delete m_val;
std::cout << "destruct" << std::endl;
}
int* m_val = nullptr;
};
int main()
{
Test t1;
Test t2(std::move(t1));
std::cout << *t2.m_val << std::endl;
return 0;
}
运行结果:
在上边的代码中,通过std::move()将t1转换成一个右值,用以初始化t2,会调用移动构造。
对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。需要注意的是,我们一般在提供移动构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数。
编译器会默认为用户在自定义的 class 或 struct 中生成移动语义函数,前提是用户没有主动定义该类的拷贝构造等函数。因此,当可移动对象在需要拷贝且拷贝后不再被需要时,使用std::move()函数提高性能。
1.3.2 完美转发
完美转发这个概念用在函数模板中,意思是指将自己的参数完美地转发给内部调用的其他函数。所谓完美,是指不仅能准确转发参数的值,也可以转发它的属性(左值还是右值)。所谓转发,是指类型的转换而不是真转发了什么东西。
借助万能引用(T&&)进行类型推导,通过引用的方式接收左右属性的值,根据引用折叠,利用forward指明模板参数,这样才构成一套完整的完美转发机制。
关于引用折叠:
- T& & -> T&
- T& && -> T&
- T&& & ->T&
- T&& && ->T&&
根据上述规则可以得到:使用T&&可以区分入参的左右值属性,这就是万能引用的由来。
std::forward()函数的定义如下:
template<class T>
constexpr T&& forward(std::remove_reference_t<T>& arg) noexcept{
// forward an lvalue as either an lvalue or an rvalue
return (static_cast<T&&>(arg));
}
template<class T>
constexpr T&& forward(std::remove_reference_t<T>&& arg) noexcept{
// forward an rvalue as an rvalue
return (static_cast<T&&>(arg));
}
它接受一个参数arg,根据arg的左右值属性决定将其转发为左值引用还是右值引用。如果arg为左值,它会将其转化成右值返回,这样即可传给左值也可传给右值;如果arg为右值,它保留其右值属性返回,这样只可以返回给右值。
完美转发例子:
#include <iostream>
class Test {
public:
Test() : m_val(new int(10)) {
std::cout << "construct" << std::endl;
}
Test(const Test& a) : m_val(new int(*a.m_val)) { //拷贝构造函数
std::cout << "copy construct" << std::endl;
}
Test(Test&& a) noexcept : m_val(a.m_val) { //移动构造函数
a.m_val = nullptr;
std::cout << "move construct" << std::endl;
}
~Test() {
delete m_val;
std::cout << "destruct" << std::endl;
}
int* m_val = nullptr;
};
template<typename T>
void forward(T&& t) {
Test a = std::forward<T>(t);
std::cout << "address: " << &a << std::endl;
}
int main() {
Test t1;
forward(t1);
forward(Test());
return 0;
}
运行结果:
根据结果不难看出一个调用了拷贝构造函数,一个调用了移动构造函数,forward函数根据传入的参数完成了转发。
2. move和forward函数的区别
虽说forward函数可以同时处理左值和右值,看起来要比move函数厉害。但“存在即合理”,move函数肯定是有用武之地的,不然标准库就不会用它了。
使用move函数的原因:
1. forward函数常用于模板函数这种入参情况不确定的场景中,使用时要带模板参数<T>。
2. 当确定需要临时值的时候,使用move更简洁方便,使用forward会导致代码意图不清晰。