一:什么是左值,是什么是右值
- 左值:顾名思义,左值就是可以放在等号左边的值、具名(具备名称)、能够取地址。如:变量名、返回左值引用的函数调用、前置自增\自减、赋值运算或符号赋值运算、解引用等等。
- 右值:和左值相反,只能在等号右边、不具名、不能取地址。
- 纯右值:字面值(int i= 1)、返回非引用类型的函数调用、后置自增\自减、算术表达式、逻辑表达式、比较表达式。
- 将亡值:c++11新引入的与右值引用(移动语义)相关的值类型。将亡值用来触发移动构造或移动赋值构造,并进行资源转移,之后将调用析构函数。
区分表达式的左右值属性:如果可对表达式用
&
符取址,则为左值,否则为右值。
二:左值引用和右值引用
形式定义:
//左值引用:type &引用名 = 左值表达式
int i = 42;
int &r = i;//正确,左值引用
int &r1 = i * 42; //错误, i*42是一个右值
const int &r = i*42;//正确,可以将一个const的引用绑定到一个右值上
//右值引用:type &&引用名 = 右值表达式
int i = 42;
int &&r = i; //错误,i是一个变量,变量都是左值
int &&r = i *42; //正确,i*42是一个右值
区别:
- 左值引用就是对左值的引用,右值引用就是对右值的引用;声明出来的左值引用和右值引用都是左值,因为引用本身就是一个变量。
- 功能差异
- 左值引用:左值引用是为了避免对象的拷贝;
- 右值引用:右值引用时为了实现移动语义和完美转发。
三:移动语义和完美转发
移动语义
在对象赋值时,避免资源的重新分配;通过出发移动构造来实现移动语义(基于move)。以移动而非深拷贝的方式初始化含有指针成员的类对象。移动语义值将其他对象(通常是临时对象)拥有的内存资源移位己有。
class A
{
public:
A() {
p = new int(10);
};
//拷贝构造 深拷贝
A(const A &a) {
//这里来尝试实现移动语义
//p = a.p;
//因为a是const,所以这里不成功,之所以要赋值为空,防止析构时p成为野指针
//a.p = nullptr;
p = new int(10);
memccpy(p, a.p, 10 * sizeof(int));
};
~A() {
if (p!= nullptr)
{
delete[]p;
p = nullptr;
}
};
//移动拷贝构造
A(A &&a) {//这里传入的值就是将亡值
this->p = a.p;
a.p = nullptr;
};
int *p;
};
int main()
{
A a;
//使用移动构造
A b(std::move(a));
return 0;
}
完美转发
完美转发基于万能引用,引用折叠以及std::forward模板函数。
定义:函数模板可以将自己的参数完美的转发给内部调用的其他函数;完美转发指不仅能准确的转发参数的值,汉能保证被转发的参数的左右值属性不变。
借用万能引用,通过引用的方式接收左右值属性的值。
void func(int &n)
{
//...
}
void func(int &&n)
{
//...
}
template<typename T>
void revoke(T &&t)
{
func(forward<T>(t));
}
万能引用
谈及万能引用得谈谈“&&”的特性了;
“&&”特性
右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内 存,只是该对象的一个别名。
通过右值引用的声明,该右值又“
重获新生
”
,其生命周期其生命周期与右值引用类型变量的生命周期一
样,只要该变量还活着,该右值临时量将会一直存活下去。
&&
的总结如下:
- 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值。
- auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal references-万能引用。它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。
- 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引 用。当 T&& 为 模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右 值引用。
- 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。
引用折叠
参数为左值或左值引用,万能引用T&&将转换为左值引用int&;为右值或右值引用,将转换为右值引用int &&;
- 所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&)
- 所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)
引用折叠解决参数接收的问题。
template<typename T>
void revoke(T &&t)
{
func(forward<T>(t));
}
revoke(static_cast<int &>n);
//编译器会将int & &&t折叠为int &t;
revoke(static_cast<int &&>n);
//编译器会将int && &&t折叠为int &&t;
revoke(n);
//编译器会将int &&t折叠为int &t;
//解决透传问题
forward<T>v;
//T为左值引用,v将转换为T类型的左值;T为右值引用,v将转换为T类型的右值;