左值与右值
左值和右值是C语言中的概念,但是C语言并没有很好的给出他们的区分方式,所以在早期时通常认为在等号的左边就是左值(能够取地址),在等号的右边就是右值(不能取地址)。但是这样的说法是不够准确的。
例如这一段,a和b很明显都是左值,但是他们都能放在等号的左右两端。
int main()
{
int a = 1;
int b = 2;
a = b;
b = a;
return 0;
}
所以不能简单的根据等号的左右侧来进行划分,而是要根据表达式的结果或者变量的性质进行划分。
现代通常按照以下标准进行划分
- 普通类型的变量,因为有名字,可以取地址,都认为是左值。
- const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是
const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),
C++11认为其是左值。 - 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
- 如果表达式运行结果或单个变量是一个引用则认为是左值。
而在c++11中,考虑到引入移动语义,又将右值划分为两个类别
- C语言的纯右值(如普通类型的常量(const算左值))
- 将亡值(例如函数表达式的中间结果,临时变量)
而右值引用顾名思义,就是引用一个右值,为了与左值引用进行划分,使用一个&时则代表是左值引用,而使用两个&&则代表右值引用。
例如:int&& rRef = Add(3, 4);
右值引用与左值引用
int main()
{
int&& rRef = 8;
int& lRef = 7;//报错,普通引用不能引用右值
const int& lRef2 = 9; //const左值引用既可以引用左值也可以引用右值
}
从上面的代码可以看到,普通的左值引用只能引用左值,而const的左值引用既能引用左值,又能引用右值。那么为什么C++11还需要引入右值引用的概念呢?这就涉及到了移动语义的问题。
移动语义
移动语义:将一个对象中的资源移动到另一个对象中,通常用来移动一个将亡值。
这里就拿STL中的string来举例子。
C++ STL : 模拟实现STL中的string类
移动语义最常用的地方就是拷贝构造和赋值运算符重载。
例如string中的拷贝构造函数
string(const string& s)
: _size(s._size)
, _capacity(s._capacity)
, _str(new char[_size + 1])
{
strcpy(_str, s._str);
}
可以看到,这里首先这里其实是很传统的写法,开空间,然后把数据拷贝过来,这样的代码对于左值来说完全没有什么问题,而对于右值,则多了完全不必要的操作,因为如果拷贝构造的对象是一个将亡值,那就代表着他的空间马上就要销毁,这时我们开空间拷贝一个将要销毁的数据,是效率很低的一件事,所以我们可以不用想往常一样操作,而是直接将他的空间拿过来,这样效率就大大的提高了。
string(string&& s)
: _size(s._size)
, _capacity(s._capacity)
, _str(nullptr)
{
std::swap(_str, s._str);
}
这就是通常所说的移动构造,就是在进行拷贝构造时,如果拷贝构造的对象是一个将亡值,就直接将他的资源移动到我们的对象中即可。
同样的,移动赋值也是这样的原理。
string& operator=(string&& s)
{
_size = s._size;
_capacity = s._capacity;
std::swap(_str, s._str);
return *this;
}
这样光说可能看不出他的具体效果在哪里,下面就举个更明显的例子。
string operator+(const string& s2)
{
string ret(*this);
return ret;
}
int main()
{
lee::string s1("hello");
lee::string s2("world");
lee::string s3("111");
s3 = s1 + s2;
return 0;
}
接下来看看加入了移动构造和移动赋值后的效果
可以看到,因为这里+运算的返回值和(s1+s2)都是右值,所以这里和上面不同的是进行了一次移动构造和移动赋值,避免了对将亡值的数据拷贝,导致效率大幅度的提升,这也就是为什么c++11在有了const&的情况下还要增加右值引用的原因,就是为了单独处理将亡值的移动问题。
总结一下就是,需要通过右值引用来划分开左值的拷贝语义和右值的移动语义。
move
按照语法来说,右值引用应该只能引用右值,但是在很多情况下,我们需要使用引用左值来实现移动语义,所以为了实现这个功能,c++11又加入了一个move函数。这个函数的主要作用就是将一个左值强行转换为右值(STL还有另一个move,那个的作用就是将一个范围中的元素搬移到另一个位置,容易搞混)
例如
int main()
{
lee::string s1("hello");
lee::string s2(move(s1));
}
需要注意的是,被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。但是在其进行完移动语义后,原本的资源被转移到别的对象中,此时他的资源就会失效
例如:
int main()
{
string s1("hello");
cout << s1 << endl;
string s2(move(s1));
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
s2将s1转为右值后进行移动构造之后,s1的资源失效。
完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
void Fun(int &x){ cout << "lvalue ref" << endl; }
void Fun(const int &x){ cout << "const lvalue ref" << endl; }
void Fun(int &&x){ cout << "rvalue ref" << endl; }
void Fun(const int&& x){ cout << "const rvalue ref" << endl; }
template<typename T>
void Forward(T&& t) {
Fun(t);
}
int main()
{
PerfectForward(10); // rvalue ref
int a = 3;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
例如我们使用一个普通的转发,Forward是一个模板,用来转发参数到Fun中,下面来看看转发的效果
可以看到,经过一层转发之后,原本的右值属性全部丢失,都变为了左值。而我们希望的是将原本的参数类型在不进行任何变化的情况下转发给目标,中间没有任何丢失以及损耗,原本是左值就是左值,原本是右值传递后也该是右值。这样做的目的是保证根据原来的参数来实现不同的语义,例如左值实行拷贝语义,右值实行移动语义。
所以c++11引入了一个模板函数forward来实现完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
只需要在转发时加上forward来进行一层包装处理,此时就能实现完美转发了。