右值
目录
左值和右值右值引用引用坍缩完美转发左值和右值
左值和右值从最基本的书面意思上理解就是:
表达式左边的值为左值,表达式右边的值为右值
int x = 1;int y = 2;int z = x + y;
这里x、y、z是左值,1、2和x+y的结果也是右值,但这样的分类并不标准:
int a = 1;int b = a;
这时,a是不是可以看成左值也可以看成右值;与我们上面的简单分类就矛盾了。
C++中的左值一般是指一个指向特定内存的具名对象,它有一个具体稳定的内存地址和较长的生命周期。
而右值则是不指向稳定内存地址的匿名值,即不具名对象。
那么基于这个特性,我们可以用&
引用符号来判断左值和右值,能取到地址的为左值,否则为右值。
不过,C++标准的对类型的定义更复杂:
lvalue
是通常可以放在等号左边的表达式,左值rvalue
是通常只能放在等号右边的表达式,右值glvalue
是generalized lvalue
,广义左值xvalue
是expiring lvalue
,将亡值prvalue
是pure rvalue
,纯右值
我们先看lvalue
和prvalue
:
lvalue
就是上面提到过的,可以通过取地址来判断的左值,如:
变量,函数返回左值引用的表达式:++x
、x = 1
字符串字面量:📌这里x++是右值,而++x是左值;因为在后置++操作中编译器首先会生成一个x的临时复制,然后才对x递增;而++x则是直接对x递增然后返回自身,即原地操作。
”hello“
而纯右值prvalue
是没有标识符的,不能取地址,一般也称为”临时对象“,比如:
返回非引用类型表达式:x++
,x + 1
除字符串字面量之外的字面量:42
、true
在C++11之前,右值可以绑定到常左值引用的参数,如const T&
;但不能绑定到非常值左值引用,如T&。
这里绑定可以简单理解为赋值,即
const int& ref = 42
右值引用
从C++11开始,C++就引入了一种新的引用类型——右值引用;右值引用的形式是T&&
右值引用,顾名思义就是一种引用右值的方法。
那么,引用右值将要解决的问题是什么?
简单来看,有了右值引用,我们就可以把右值绑定到右值引用上,进而延长右值的生命周期。
我们先来看一下移动语义,移动语义的原理是允许在对象之间转移资源的所有权,而不是进行深拷贝;我们通常用std::move
来实现移动语义。
左值引用在传递参数或返回值时,通常调用拷贝构造函数或赋值构造函数,来创建对象的副本。
而右值引用引入了移动构造函数和移动赋值函数,它们可以直接将对象的所有权转移到另一个容器中。
class MyData {public:MyData() {}// 移动构造函数MyData(MyData&& other) noexcept {}// 移动赋值函数MyData& operator=(MyData&& other) noexcept {}};
所以右值引用的作用不只是延长右值的生命周期,更重要的是这种机制可以用来减少对象的拷贝。
了解了右值引用的作用后,再来看std::move
,它的作用就是将传入的参数强制转换成右值引用;那就可以有这样的表达式std::move(ptr)
,它可以看作一个有名字的右值,为了和纯右值prvalue
做区分,C++就把这种表达式叫做xvalue(将亡值)
。
但是xvalue
还是和左值lvalue
不同,不能取地址。
上面我们讨论的这些左值、纯右值、将亡值,都属于值类别;而值类型才是指我们平时常见的int,double,枚举这些类型。
引用坍缩
常左值引用既可以引用左值又可以引用右值,是一种几乎全能的引用;不过其常量性还是限制了作用范围。
C++11标准中有着一个能称为万能的引用,它的形式是:
T &&auto &&
所谓的万能引用是因为发生了类型推导,T&&
和auto&&
的初始化过程中都会发生类型的推导
C++中添加了一套引用叠加推导的规则——引用坍缩,有着如下规则:
下面对上述规则给出例子:
左值引用和右值引用坍缩成左值引用:
template <typename T>void foo(T& arg) {// 在这里,arg被视为左值引用,即使传入的是右值}int main() {int x = 42;foo(x); // T被推导为int,arg是int&,因此arg被视为左值引用foo(10); // T被推导为int,arg是int&,因此arg被视为左值引用return 0;}
右值引用和左值引用坍缩成右值引用:
template <typename T>void bar(T&& arg) {// 在这里,arg被视为右值引用,即使传入的是左值或右值}int main() {int x = 42;bar(x); // T被推导为int&,arg是int& &&,因此arg被视为int&bar(10); // T被推导为int,arg是int&&,因此arg被视为右值引用return 0;}
完美转发
之前提到了万能引用的语法和推导规则,现在来谈一下它的用途,完美转发。
我们先来看一个转发函数模板:
template<class T>void show_type(T t){std::cout << typeid(t).name() << std::endl;}template<class T>void normal_forwarding(T t){show_type(t);}int main(){std::string s = "hello world";normal_forwarding(s);}
normal_forwarding
是一个转发函数模板,可以做一些字符串的转发任务;不过因为是按值转发,会发生一次临时对象的复制。
为了解决这个问题可以把normal_forwarding(T t)
替换为normal_forwarding(T& t)
,这样能够避免临时对象的复制。
不过又会带来另一个问题,如果传递的是一个右值,将无法编译。
这时还可以将normal_forwarding(T& t)
替换为normal_forwarding(const T& t)
,因为常量左值引用时可以引用右值;但是如果之后要修改这个字符串,又会编译错误。
万能引用的出现解决了这个问题,对于万能引用的形参来说:
如果实参是左值,则形参会被推导为左值引用;如果实参是右值,则形参会被推导为右值引用;
所以无论传递的是左值还是右值都可以被转发,不会产生多余的拷贝。