C++ 右值引用
block://6984617523950616580?from=docs_block&id=ce31003ceb5efb1f7a7c0a5fbe6cb60191627a38
右值的引入
作为在C++11中引入的一个类型,容易引起误解的是,右值引用并没有说明引入是为了什么,是为了解决什么问题。
右值引用可以解决以下问题
- 实现移动语义
- 完美转发
左值和右值来自原先的C语言,左值可以出现在赋值左边或者右边,而右值只能出现在赋值的右边
int a = 42;
int b = 43;
a = b;
b = a;
a = a * b;
int c = a * b;
a * b = 42;
在 C++ 中,这作为第一个直观的左值和右值方法仍然很有用。但是,带有用户定义类型的 C++ 引入了一些关于可修改性和可分配性的微妙之处,导致此定义不正确。我们没有必要进一步讨论这个问题。这是一个替代定义,尽管它仍然存在争议,但它将使您能够处理右值引用:左值是一个引用内存位置的表达式,并允许我们通过& 操作符取得地址,右值,不是左值的都是右值。
int i = 42;
i = 43;
int* p = &i;
int& foo();
foo() = 42;
int* p1 = &foo();
int foobar();
int j = 0;
j = foobar();
int* p2 = &foobar();
j = 42;
移动语义
假设有一个类X,类中的成员变量m_pResource是一个需要花费时间和内存取进行构造和析构的类型,比如m_pResource是一个vector 类型,对其进行赋值时将会产生大量的析构和构造函数的调用。
X& X::operator=(X const & rhs)
{
}
同样的问题会出现在copy 构造函数上
X foo();
X x;
x = foo();
-
clones the resource from the temporary returned by foo , -
destructs the resource held by x and replaces it with the clone, -
destructs the temporary and thereby releases its resource.
当赋值操作符的右边是右值的话,只是交换值的指针是比较高效的
上述这种操作就是移动语义,可以通过操作符重载实现
X& X::operator=(<mystery type> rhs)
{
}
以上调用无论是赋值还是copy 构造函数,都会导致大量的构造函数和析构函数调用(如当vector中存储很多的类对象时),因此我们当然希望能够实现对传入类型的引用,从而避免这些构造函数和析构函数的调用
block://6984620384730546178?from=docs_block&id=ce31003ceb5efb1f7a7c0a5fbe6cb60191627a38
右值引用
如果X是一个类型,那么X&& 就是对X类型的右值引用,为了更好的区分X&被称为左值引用
一个右值引用类型很多地方表现与左值引用相同,除了一些例外。最重要的一条就是,当进行函数重载的时候,左值当成参数传入函数,偏向调用左值引用的函数;当右值传入函数时,更加偏向调用右值重载的函数
void foo(X& x);
void foo(X&& x);
X x;
X foobar();
foo(x);
foo(foobar());
Rvalue references allow a function to branch at compile time (via overload resolution) on the condition “Am I being called on an lvalue or an rvalue?”
大体意思就是,右值引用允许编译器期间通过是右值还是左值调用不同的函数
当然你可以使用上述方法重载任何函数,就像上述所示。但是通常会被用于重载拷贝构造函数和赋值构造函数,用来实现移动语义
X& X::operator=(X const & rhs);
{
return *this;
}
Note: If you implement
void foo(X&);
but not
void foo(X&&);
then of course the behavior is unchanged: foo can be called on l-values, but not on r-values. If you implement
void foo(X const &);
but not
void foo(X&&);
then again, the behavior is unchanged: foo can be called on l-values and r-values, but it is not possible to make it distinguish between l-values and r-values. That is possible only by implementing
void foo(X&&);
as well. Finally, if you implement
void foo(X&&);
but neither one of
void foo(X&);
and
void foo(X const &);
then, according to the final version of C++11, foo can be called on r-values, but trying to call it on an l-value will trigger a compile error.
强制移动语义
我们都知道,在给予更多控制权和避免粗心大意犯错方面C++选择给予更多的控制权,你不但可以在右值上实现移动语义,而且你可以自行决定在左值上实现移动语义,一个很好的例子就是std::swap函数
template<class T>
void swap(T& a, T& b)
{
T tmp(a);
a = b;
b = tmp;
}
X a, b;
swap(a, b);
这里没有使用右值,因此有没有实现移动语义,但是我们知道实现移动语义会更好,只要变量作为复制构造或者赋值的源出现,该变量要么根本就不再使用,要么就作为赋值的目标。
C++11中与一个被调用的库函数std::move 可以将其参数转换 右值, 不做其他事情
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
X a, b;
swap(a, b);
修改之后上述三行实现了移动语义,需要注意的是,对于那些没有实现移动语义的类型(即:没有使用右值引用版本重载它们的复制构造函数和赋值运算符),对于这些类型新的swap就和旧的一样
既然、知道了移动语义std::move ,如下:
a = b;
你期望在这里发生什么?你期望a持有的对象被b的复制出来的副本替换,并且希望a先前持有的对象析构,现在我们考虑一下语义:
a = std::move(b);
如果实现了移动语义,会交换a和b持有的对象,不会有任何对象进行析构。当然结束之后a原先持有的对象的生命周期将和b的作用范围绑定,b超出范围a原先持有的对象将会被销毁。
所以从某种意义上说,我们在这里陷入了非确定性破坏的阴暗世界:一个变量已被分配,但该变量以前持有的对象仍在某处。只要该对象的销毁不会产生任何外界可见的副作用,就可以了。但有时析构函数确实有这样的副作用。一个例子是释放析构函数内的锁。因此,具有副作用的对象销毁的任何部分都应该在复制赋值运算符的右值引用重载中显式执行:
X& X::operator=(X&& rhs)
{
return *this;
}
右值引用就是右值吗?
像以前一样,我们为X实现复制构造函数和赋值操作符重载来实现移动语义。
假如:
void foo(X&& x)
{
X anotherX = x;
}
代码中函数内x是一个左值引用,然而我们期望让右值引用就是本身就是右值。右值引用的设计者提供了一个更好的思路:
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.
大意就是,右值引用可以是左值也可以是右值,评判的标准是,如果这个值有命名就是左值,如果没有就是右值。
那么上述代码中,虽然参数传进的是右值,但是进入函数的时候,因为x已经有命名了,所以函数内部的x是左值,那么函数内部调用的也是左值的赋值函数
void foo(X&& x)
{
X anotherX = x;
}
如下是一个没有名字的右值,因此会调用右值赋值函数
X&& goo();
X x = goo();
这种设计的背后思路就是:允许移动语义应用于一些有名字的对象
X anotherX = x;
以上语句是非常危险的,移动的食物应该在移动后立即死亡并消失,因此有一条规则,如果它有一个名字,那么它就是左值
|