今天总结下右值的那些事儿
- 什么是右值
- 右值的必要性
- move函数
什么是右值
传统c++的引用就是左值引用,使得标识符关联到左值。左值是一个表示数据的表达式,如变量名或指针等,程序可以获取其地址。
右值引用使用 &&
来表示,右值引用可关联到右值,即可出现在赋值表达式的右边,但不能对其应用地址运算符的值。
左值其实就是可以通过内存地址访问的值,而右值则相反,是不能通过地址访问的值
说了这么多貌似还是不知道右值是什么,右值一边有这三种类型:
- 字面常量
- 诸如 x+y 等表达式
- 返回值的函数(该函数返回的不是引用,也不是指针,直接返回对象那种)
值分左右
C++ 里有左值和右值。这话不完全对。标准里的定义实际更复杂,规定了下面这些值类别(value categories):
- lvalue:通常是可以放在左边的表达式,左值
- rvalue:通常是只能放到右边的表达式,右值
- glvalue:generalized lvalue,广义左值
- xvalue:expiring value,将亡值
- prvalue:纯右值
我们先看lvalue和prvalue。
左值lvalue,是有标识符,可以取地址的表达式
- 变量、函数或数据成员的名字
- 返回左值引用的表达式,如 ++x、x = 1、cout << ' '
- 字符串字面量如 "hello world"
在函数调用的时候,左值可以绑定到左值引用的参数,如T&,而常量只能绑定到常左值引用 const T&
反之,右值就是没有标识符,不可以取地址的表达式
- 返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
- 除字符串字面量之外的字面量,如 42、true
注意,返回为非引用类型的表达式就是右值,而引用类型只有两种类型,指针和引用,其它都是非引用类型。make_shared返回的是一个智能指针,其实也是一个智能指针的对象,不是引用型,当然也是右值。还有一点需要注意的是,对于指针通常采用值传递,所以我们并不关心是左值还是右值
smart_ptr ptr2 = std::move(ptr1);
std::move(ptr),它的作用就是把一个左值强制转换成一个右值引用,但并不改变它的内容。我们可以把std::move(ptr)看成一个有名字的右值,c++里,这种就属于xvalue,将亡值
右值的必要性
假设现在有如下代码:
函数 allcaps 并没有返回引用或指针,直接在函数体内构造了一个临时变量并且返回,临时变量是会被删除的。但vstr_copy2依然会调用复制构造函数来构造一个新的对象,如此,上述过程中,先是构造了一个临时变量 temp,再调用复制构造函数生成了一个新对象 vstr_copy2,再删除了temp,temp的生命周期非常非常短,没有什么作用,中间还产生了大量的内存操作。是否可以避免多作的内存操作呢?
是的,可以的,右值引用的作用就体现了。右值引用搭配移动构造函数就能起到这样的效果。让我们来看一个例子:
class Useless
{
public:
Useless();
~Useless();
explicit Useless(int k);
Useless(int k, char ch);
Useless(const Useless& f);
Useless(Useless&& f);
Useless operator+(const Useless& f) const;
void showData() const;
private:
int n;
char* pc;
static int ct;
void showObject();
};
int Useless::ct = 0;
Useless::Useless()
{
++ct;
n = 0;
pc = nullptr;
cout << "default constructor called, number of objects : " << ct << endl;
}
Useless::Useless(int k) : n(k) {
++ct;
cout << "int constructor called, number of objects : " << ct << endl;
pc = new char[n];
showObject();
}
Useless::Useless(int k, char ch) : n(k) {
++ct;
cout << "int char constructor called, number of objects : " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
{
pc[i] = ch;
}
showObject();
}
Useless::Useless(const Useless& f) :n(f.n) {
++ct;
cout << "copy constructor called, number of objects : " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
{
pc[i] = f.pc[i];
}
showObject();
}
Useless::Useless(Useless&& f) : n(f.n) {
ct++;
cout << "move constructor called, number of objects : " << ct << endl;
pc = f.pc;
f.pc = nullptr;
f.n = 0;
showObject();
}
Useless::~Useless()
{
cout << "destroy called, object left" << --ct << endl;
cout << "delete object: \n";
showObject();
delete[] pc;
}
Useless Useless::operator+(const Useless& f) const {
cout << "enter operator +" << endl;
Useless temp = Useless(n + f.n);
for (int i = 0; i < n; i++)
{
temp.pc[i] = pc[i];
}
for (int i = n; i < temp.n; i++)
{
temp.pc[i] = f.pc[i - n];
}
cout << "temp object : \n";
cout << "leaving operator +" << endl;
return temp;
}
void Useless::showObject() {
cout << "num of elements" << n;
cout << "data address " << (void*)pc << endl;
}
void Useless::showData() const{
if (n == 0) {
cout << "empty object";
}else{
for (int i = 0; i < n; i++)
{
cout << pc[i];
}
cout << endl;
}
}
如上所示,这么来调用:
Useless four(one + three);
one+three,第一小节中有提到过,这是一个右值,然后会再调用
Useless(Useless&& f);
这种函数称为移动构造函数,它的特殊之处在于,不会重新地进行内存操作,为 pc 指针进行内存初始化,而是直接将 右值引用 的pc内存地址赋值给自己,这样就省去了一次内存操作了,然后将右值的pc指针指向空指针,这是因为,一个内存如果有两个指针指向它,如果另一个对象被析构了,pc被删除了,另一个pc指针就会成为野指针,不好跟踪,所以一般地移动构造函数都会将右值指针清空。
生命周期
一个变量的生命周期在超出作用域的时候就会结束,那么纯右值呢?
C++ 的规则是:一个临时对象会在包含这个临时对象的完整表达式估值完成后、按生成顺序的逆序被销毁,除非有生命周期延长发生。
class shape {
public:
virtual ~shape(){};
};
class circle: public shape {
public:
circle(){
cout << "create circle" << endl;
}
~circle(){
cout << "delete circle" << endl;
}
};
class triangle: public shape {
public:
triangle(){
cout << "create triangle" << endl;
}
~triangle(){
cout << "delete triangle" << endl;
}
};
class result{
public:
result(){
cout << "create result" << endl;
}
~result(){
cout << "delete result" << endl;
}
};
result process_result(const shape& shape1, const shape& shape2){
return result();
}
void test_rvalue(){
cout << "test_rvalue" << endl;
process_result(circle(), triangle());
cout << "end" << endl;
}
上述示例程序中,process_result函数中传入的两个参数是临时对象(纯右值),因为它们返回的并不是引用(circle和triangle的构造函数返回的并不是引用类型),这些临时对象的生命周期只在自己的那一行,process_result函数调用完它们的生命周期就结束了。如果我们改一下,变成
result&& rvalue = process_result(circle(), triangle());
//日志:
test_rvalue
create circle
create triangle
create result
delete triangle
delete circle
end
delete result
从日志上看,result对象的生命周期被延长到了整个大括号内
如果一个 prvalue 被绑定到一个引用上,它的生命周期则会延长到跟这个引用变量一样长。
注意,生命周期延长,只对prvalue有效,将亡值无效
引用折叠
左值引用和右值引用作为参数表示方式分别为:
template <class T>
void f(T&);
template <class T>
void f(T&&);
在调用函数f的时候,我们可能传入一个左值,也可能传入一个右值。
由于存在T&&这种未定的引用类型,当它作为参数时,有可能被一个左值引用或者右值引用的参数初始化,这时候经过类型推导的T&&类型相比右值引用会发生变化。
具体规则是:
1.所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&)
2.所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)
move函数
导入 intility 头文件,即可以使用move函数,move函数在 std 的命名空间当中。它的作用就是,把一个左值强制转换为右值。如果构造函数的实参在使用完之后就可以丢弃,那就可以把此左值强制转换化右值,使用移动构造函数,这样就会节省一些内存操作。
注意,move完之后的参数,只能丢弃或者重新赋值,不能再使用了。