值与引用,准确的说是C++11后的值与引用。
因为这时候才明确的出现了右值引用,然后能讨论的东西就出来了,
那就是 左值、右值当然还有对应的左值引用和右值引用。
我第一次接触右值引用的时候是在看SFINAE相关方面的内容时,
当我第一次看到了 auto&& 这种写法,一脸蒙蔽,这是啥啊?
好在有标准文档的帮助,让我了解到了右值引用,但随之而来的,移动语义和完美转发等术语真的让我头大。
可不懂就得学啊,经过一段时间的摸索,终于对这些概念有了较清楚的认识了。
左值右值,通俗的讲就是左值是可以出现在等号的左边,能够被赋值,除了左值以外都是右值,也可以理解为出现在等号右边的值。
铺开来讲,你可能听说过:亡值,泛左值,纯右值 等术语。
有以下定义:
在c++11以后,表达式按值类别分,必然属于以下三者之一:左值(left value,lvalue),将亡值(expiring value,xvalue),纯右值(pure rvalue,pralue)。其中,左值和将亡值合称泛左值(generalized lvalue,glvalue),纯右值和亡值合称右值(right value,rvalue)。见下图,
左值右值有很多种方法解释:
除了上面说的通俗的说法,还有
左值:可以取地址并且有名字的东西就是左值。
右值:不能取地址的没有名字的东西就是右值。
还有
当一个对象被用作右值的时候,用的是对象的值(内容);
当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
总之,单说一个变量是左值右值需要根据上下文分析,才能分析出来,真正有意义的是结合引用。
=========================================================================
引用:
T& 引用是在最原始的c++98就存在着,就是左值引用而T&& 是C++11新出现的概念,表示右值引用。
左值引用能绑定到左值上,右值引用能绑定到右值上,这是理所当然的事情,没什么可详细讨论的。
这里再介绍一些别的内容:
引用符合叠加性:
std::add_lvalue_reference<T&>::type 是 T&
std::add_lvalue_reference<T&&>::type 是 T&
std::add_rvalue_reference<T&>::type 是 T&
std::add_rvalue_reference<T&&>::type 是 T&&
也就是说
T&& & => T&
T&& && => T&&
T& & => T&
T& && => T&
但是代码里不能这么写(vs 2017)
int b = 10;
int&& &a = b; //Error,但右值引用的左值引用是左值引用,理论上可以绑定到b上,
不过这么写就可以
typedef int&& intR;
typedef intR& intRR;
int main()
{
int &&_42 = 42;
int b = 10;
intRR a = b; //编译成功
}
移动语义和完美转发:
这两个含义各有一个标准库的代表,std::move 代表了移动语义,std::forward 代表了完美转发。
std::move产生一个对象的将亡值,以供合理的调用对象的移动构造函数用。
std::forward 下面的列子里会提到为什么要有完美转发。
================================================================
那么一个终极问题:
右值引用是左值还是右值?
答案是:看情况。
在一篇文章里说到
Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.
也是说右值引用是否有名字,来判定是左值还是右值。
测试代码如下:
class BASE
{
public:
BASE() {};
BASE(BASE const& rhs) {
cout << "aaaa\n";
};
BASE(BASE && rhs) {
cout << "bbbb\n";
};
};
class Derived :BASE
{
public:
Derived() {};
Derived(Derived const& rhs) :BASE(rhs) {};
Derived(Derived&& rhs):BASE(rhs) {
cout << "ddddd\n";
};
};
int main()
{
Derived temp(std::move(Derived()));
}
std::move(Derived()) 返回值是右值引用,但是Derived temp(std::move(Derived())); 并没有新“名字”接受std::move(Derived())产生的结果,所以,右值引用是右值,会调用Derived(Derived&& rhs)重载,但是在调用构造函数时,这个右值引用被赋予了名字“rhs”,BASE(rhs)此时变成了左值,BASE结果就调用了BASE(BASE const& rhs) 构造函数。
这个例子就阐述了为什么需要完美转发,可能本意就是想调用基类的移动构造函数,但是由于右值引用值的特性导致了一般想当然的写法无法实现。此时就需要用到了std::forward
Derived(Derived&& rhs):BASE(std::forward<Derived>(rhs)) {
cout << "ddddd\n";
};
这样就可以调用基类的移动构造函数了。