一、右值
左值、右值。顾名思义,就是可以出现在=(赋值运算符)左边和右边的值。看下面几个例子:
int func(){ return 2; }
int main(){
func() = 2;
return 0
}
// 编译错误:
// error: lvalue required as left operand of assignment
// =操作符的左边必须是左值。
// **********************************************************************
int& func(){
return 2;
}
int main(){
return 0;
}
// 编译错误
// error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
// 右值赋值给非const引用是不正确的初始化。
- 左值
- 最先在C中定义,“可以出现在=操作符左边的值”。
- 随着C特性的增加,有些特殊的左值不能出现在=操作符的左边。
- 数组类型
- 不完整类型
- const类型
- 若是struct、union,则只要它的任意成员(递归地)含有const。
- 左右值很难有一个精确定义,可以按以下理解:
- 左值是一个表示内存位置的表达式,可以通过&取地址符获得内存地址。
- 右值
- 表达式不是左值,就是右值。也就是说上面的这些特殊的左值也是右值。
- 左值可以代替右值,反过来不行,这很好理解,可以直接去左值内存里的值(就是右值了)。
int var; // 左值
var = 4; // 4,右值。
4 = var; // 错误,4是右值
(var + 1) = 2; // 错误,var+1的值是右值。
const int var = 4; // 右值,不可修改的左值是右值。
int a = 42;
int b = 43;
// a、b都是左值
a = b; // ok
b = a; // ok
a = a * b; // ok
// a * b是右值
int c = a * b; // ok, rvalue on right hand side of assignment
a * b = 42; // error, rvalue on left hand side of assignment
// 左值:
//
int i = 42;
i = 43; // ok, i is an lvalue
int* p = &i; // ok, i is an lvalue
int &foo();
foo() = 42; // ok, foo() is an lvalue
int* p1 = &foo(); // ok, foo() is an lvalue
// 右值:
//
int foobar();
int j = 0;
j = foobar(); // ok, foobar() is an rvalue
int* p2 = &foobar(); // error, cannot take the address of an rvalue
j = 42; // ok, 42 is an rvalue
// *****************************************************************
// 右值可以用左值代替
int a = 1; // a 是左值
int b = 2; // b 是左值
int c = a + b; // + 需要右值,所以 a 和 b 被转换成右值
// + 返回右值
string& str = std::string(); // 错误,str是左值引用,string()是右值
// *****************************************************************
// 左值不能用右值代替
int main(){
fuck1("asdfasdf"); // 编译错误,右值不能赋值给左值引用
fuck2("asdfasdf"); // 编译通过,右值可以赋值给常量的引用。
}
// 常量引用可以用右值赋值,可以避免创建临时对象。
void fuck1(string& a) {
return;
}
void fuck2(const string& a) { // 因此C++中经常使用const类型引用。
return;
}
二、右值引用
A a;
A& ref1 = a; // 左值引用,本来就叫引用,但是为了和右值引用区别,就叫左值引用 。
A&& ref2 = a; // 右值引用
右值引用是C++11的特性。右值引用的设计目的是为了解决以下两个问题:
- 实现移动语义(Move Semantics)
- 为程序员提供一种控制函数重载的机制,根据实参是左值/右值类型,来调用对应的重载函数。
- 具体场景:为类提供移动构造/赋值函数,让类既支持对象拷贝又支持对象移动,如std::vector的对象移动可以避免创建临时对象带来的高额开销。
- 完美转发(Perfect Forwarding)
template<typename T>
class Base {
public:
Base() = default;
~Base() = default;
Base& operator=(const X&); // 拷贝赋值
private:
T* m_pResources; // 假设m_pResources是一个拷贝代价很大的数据
// 比如是一个很大的数组。
}
template<typename T>
Base& Base<T>::operator=(const Base& rhs) {
// 以下是拷贝赋值的伪代码:
if(&rhs == this) return *this; // 查重
T* tmp = copy(rhs.m_pResource); // 拷贝一份rhs的副本
clear(m_pResource); // 清除自身的数据
m_pResource = tmp; // 绑定到拷贝的副本。
return *this;
}
// *************************************************************************
Base<int> v1;
......; // 在v1中填充了数据
Base<int> v2;
v2 = v1; // 这里原本的目的是将v1的数据转移到v2中。
// 而实际上触发的是拷贝构造,因此会带来巨大的拷贝开销,大大降低性能。
假如我们是C++的设计者,我们该如何改进?我们可以这样做:再增加两个类似拷贝构造和拷贝赋值的函数,这两个函数负责完成将参数的数据转移到本对象中。如下:
template<typename T>
Base<T>::Base(UnknownType rhs) { // 构造函数,将rhs的数据转到this中,避免临时拷贝创建
......
}
// 上面这个函数其实就是移动构造函数,UnknownType其实就是右值引用(X&&)。
template<typename T>
Base& Base<T>::operator=(UnknownType rhs) {
// 以下是将rhs的数据移动到this上的伪代码:
if(&rhs == this) return *this; // 查重
swap(m_pResource, rhs.m_pResource); // 交换数据,就达到了转移数据的目的。
return *this;
}
// 上面这个函数其实就是移动赋值函数,
何时触发拷贝赋值还是新设计的函数(移动赋值函数)?那就要看赋值运算符(=)的右边是左值还是右值,当是左值时,优先匹配拷贝赋值;当是右值时,优先匹配移动赋值。
还有一个问题,虽然提供了移动接口,但是程序员好像并不能决定使用哪个,因为程序员不能决定一个值是左值还是右值。因此还需要提供一个函数move,它一定会返回一个右值:
int leftVal = 1; // leftVal是一个左值
int &&rRef = std::move(leftVal); // 正确,rRef是leftVal的右值引用。
// move返回leftVal的右值。
int && rRef = leftVal; // 错误,右值引用无法绑定到左值。
至此,程序员终于可以自己决定出发拷贝赋值还是移动赋值。
Base<int> v1;
......; // 在v1中填充了数据
Base<int> v2;
v2 = v1; // 触发拷贝赋值,因为=赋值运算符的右边是左值
v2 = std::move(v1); // 触发移动赋值,因为=赋值运算符的右边是右值引用。
注意移动赋值里的swap函数的实现:
// 普通版本的swap,并没有出现右值,因此也就不会触发T的移动构造、移动赋值。
template<typename T>
void swap(T& a, T& b) {
T tmp(a);
a = b;
b = tmp;
}
// move版本的swap
using std::move;
template<typename T>
void swap(T& a, T& b){
T tmp(move(a)); // move(a)返回右值,触发T的移动构造函数。
a = move(b); // move(b)发回右值,触发移动赋值函数。
b = move(tmp); // 触发移动赋值函数。
}
右值引用是不是右值?
答:有名字的右值引用是左值,没有名字的右值引用,才是右值,我们可以记忆为if-it-has-a-name rule。见如下代码:
void foo(Base&& x){
Base x1 = x; // 触发拷贝构造,x并不是右值。右值引用x的名字是x。
......
}
Base& goo();
Base x = goo(); // 触发移动构造,goo()返回值是右值,没有名字。
// 为什么要这样?其实这样才合理。因为foo中的x理应在整个函数体内都可以被使用。
// 若x此时是右值,而x1=x移动构造,这是一种有潜在破坏性的拷贝,x的数据很有可能已经无效。
// 这显然是不合理的,因此才有if-it-has-a-name rule。
当Derived继承Base时,且Derived也有移动构造/赋值,请注意:
class Dervied : public Base {
public:
......
Derived(Derived&& d);
Derived& operator=(Derived&& d) noexcept; // 移动构造一般不会报异常。
}
Dervied::Dervied(Dervied && d)
:Base(std::move(d)) // 正确,调用Base的移动构造
//:Base(d) // 错误,d是左值,调用Base的拷贝构造
{
}
Derived& Dervied::operator=(Derived&& d) noexcept{
Base::operator=(d); // 千万别忘了,调用Base的移动赋值。
......
}
注意!谨慎使用std::move,否则可能事与愿违(性能下降):
Base foo(){
Base ret;
......
return ret; // 触发拷贝构造。
return std::move(ret); // 性能可能还会下降。
}
// ************************************************
int main(){
auto fuck = foo();
// 在现代编译器中,这种代码会被优化,foo并不是返回一个ret的拷贝,
// 而是直接在调用处直接构造ret,因此整个过程只触发了一次拷贝构造,
// 如果使用return std::move(),反而会阻止编译器的优化。
}
2、完美转发(Perfect Forwarding)
考虑以下场景:
template<typename T, typename Arg>
T* factory(Arg arg){
return new T(arg);
}
// factory是一个常见的参数转发函数,将ar参数转发给T的构造函数。
// 显然这是很失败的factory,因为arg是值传递(多余拷贝),很快我们想到改成引用版本:
//**********************************************************************************
......
T* factory(Arg& arg){
......
}
// 这避免了值传递,但是arg的实参只能是左值
factory(foo()); // 错误,foo的返回值是右值
factory(4); // 错误,4是右值。
// 在左右值中知识中,我们知道,const Arg& 是可以赋值右值的,因此我们可以再加一个常量引用版本
//**********************************************************************************
......
T* factory(const Arg& arg){
......
}
// 到目前为止,左右值都可以转发,但是不够完美:
// 1、一个参数就有两个版本的factory,代码太多了。
// 2、arg始终都是左值,在内部并不能触发移动语义。
// 显然还是要靠右值引用。
C++的函数参数类型推导规则,包含两部分:
- 第一,引用折叠规则
- A& & 变成 A&
- A& && 变成 A&
- A&& & 变成 A&
- A&& && 变成 A&&
- 第二,模板参数推导规则(有模板的时候)
- 若实参是A类型的左值,则T推导为A&
- 若实参是A类型的右值,则T推导为A&&,见下面代码:
template <typename T>
void test( T &&val ){}
void main()
{
int i = 0;
const int ci = i;
// 实参是int的左值,T推断为int&,val的类型是int& &&,折叠为int&
test( i );
// 实参是int的特殊左值,T推断为const int&,val的类型是const int& &&,折叠为const int&
test( ci );
// 实参是int的右值,T推断为T,val的类型是T&&
test( i * ci );
return;
}
根据以上规则,我们可以设计一个函数,使得传入的是左值类型参数时,以左值引用类型参数传递,当传入的是右值类型参数时,以右值引用类型参数传递,这样的话就可以完美转发左右值参数了,如下设计:
template<typename T>
T* factory(Arg&& arg){
return new T(std::forward<Arg>(arg));
}
template<class Arg>
Arg&& forward(typename remove_refernece<Arg>::type& a) noexcept{
return static_cast<Arg&&>(a);
}
// ********************************************************************************
// 假设实参是左值类型参数,代码:
X x;
factory<A>(x);
// 根据上面的模板参数推导规则,x是X类型的左值,则Arg推断为X&,模板代码变为:
template<typename T>
T* factory(X& && arg){
return new T(std::forward<X&>(arg))
}
template<class S>
X& && forward(typename remove_refernece<X&>::type& a) noexcept{
return static_cast<X& &&>(a);
}
// ***************************************
// 根据引用折叠规则,模板代码变为:
template<typename T>
T* factory(X& arg){
return new T(std::forward<X&>(arg));
}
template<class S>
X& forward(X& a) noexcept{
return static_cast<X&>(a);
}
// 参数arg是左值类型,且在factory、forward函数中是以左值引用方式传递,避免了值拷贝
// 这是完美的左值类型参数传递(转发)方案。
// ********************************************************************************
// 假设实参是右值类型参数,代码:
X foo();
factory<A>(foo());
// 实参类型是X&&,则Arg推断为X,模板代码变为:
template<typename T>
T* factory(X&& arg){
return new T(std::forward<X>(arg));
}
template<class S>
X&& forward(X& a) noexcept{
return static_cast<X&&>(a);
}
// 参数类型是右值类型,且在factory、forward中是以右值引用方式传递,会触发转移语义
// 这正是完美的右值类型参数传递方案。
综上,这个方案确实就是完美转发方案。
回头再看下std::move的可能实现:
template<class T>
typename remove_reference<T>::type&& std::move(T&& a) noexcept
{
typedef typename remove_reference<T>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
// 通过假设两种场景来验证一下
// 场景一:传入左值类型参数
// 场景二:传入右值类型参数
// *********************************************************************************************
// 场景一:假设如下代码:
X x;
std::move(x); // 此时x是左值,看move如何返回右值引用。
// 根据上面的参数类型推导规则,实参是左值类型,T推断为X&,代码变为:
template<class T>
typename remove_reference<T&>::type&& std::move(T& && a) noexcept
{
typedef typename remove_reference<T&>::type&& RvalRef;
return static_cast<RvalRef>(a);
}
// 进行引用折叠,去掉remove_reference,得到代码:
template<class T>
T&& std::move(T& a) noexcept
{
return static_cast<T&&>(a);
}
// ****************************************
// 场景二:假设代码如下:
X foo();
std::move(foo());
// 同上理推导出代码为:
template<class T>
T&& std::move(T&& a) noexcept
{
return static_cast<T&&>(a);
}
// 这乍一看,直接调用static_cast不就行了嘛,事实是确实可以,为了规范,还是要调用std::move
三、移动语义的except
这里十分建议你遵守以下两个规则:
- 尽力将移动构造函数/移动赋值函数设计成不会抛出异常
- 并将函数声明成noexcept。 ```cpp
template
这样建议的主要原因是,假设是std::vector,如果没有遵守上面两条规则,则std::vector gets resized的时候,不会触发转移语义,即已有元素不会relocated到新的内存块。Scott Meyers的《Effective Modern C++》中有详细说明。
<a name="nZQOm"></a>
# 四、总结
```cpp
void foo(T& t){} // t是左值时,匹配此函数
void foo(T&& t){ // 当t是右值时,匹配此函数
// 当进入到函数内,t就不再是右值,而是左值,因为t有名字(t)。
T f = std::move(t); // 需要再触发移动语义,必须显式再调用std::move
}
T t;
foo(t); // 匹配foo(T&)
foot(std::move(t)) // 匹配foo(T&&)
// std::move返回对象的右值引用。
std::forward<T&>(t); // 返回值是左值引用
std::forward<T&&>(t); // 返回值是右值引用。
std::forward<T>(t); // 返回值是右值引用。
// std::move,让程序员可以获得指定对象的右值引用
// std::forward,让程序员可以获得指定对象的左值引用或右值引用。