之前学习了C++的左值右值,但只留在了对于概念的理解上面。今天学习了一下移动构造函数,临时对象/将亡值即将被销毁时会唤起移动构造函数。还有一个std::move(),它的主要作用是将一个左值转换为右值引用,从而强制使用移动构造函数或移动赋值运算符。所以这个std::move()只是进行了一个类型转换而已,并不会触发移动操作。使用std::move()后,我们就可以调用接受右值引用的函数。

1
2
3
4
template<typename T>
constexpr typename std::remove_reference<T>::type&& move(T&& arg) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(arg);
}

再回顾一下左右值的定义:

左值:通常指的是有名字的变量或者持久对象,可以取它的地址。
右值:通常是临时对象,比如函数返回的临时对象、字面量(除了字符串字面量,它实际上是左值)等。

右值引用:使用两个&&表示,例如 int&&。右值引用可以绑定到右值,但不能绑定到左值。

我看有些教程说右值不能取地址,我一直对此非常疑惑:右值为什么不能取地址?既然存在,那肯定在内存/寄存器里,那么右值引用又是什么?

通过右值引用,我们可以延长右值的生命周期,右值引用本身是一个左值,因为它有名字。所以我们可以对右值引用取地址,这个地址就是被引用的右值对象的地址。

主要需要搞清楚两个问题:

  1. 右值引用是一个左值?是
  2. 右值引用是否在内存中创建了一个新对象,复制了即将销毁的右值?

不是的。右值引用并不创建新对象,它只是引用了那个右值(临时对象)。但是,由于右值引用本身是一个左值,所以当使用右值引用时,实际上是在直接操作那个临时对象。

1
2
3
4
5
6
int main() {
MyString&& rref = MyString("Hello"); // 右值引用绑定到临时对象
// 这里,临时对象的生命周期被延长到与rref相同
// 没有创建新对象,也没有复制数据,rref就是临时对象的别名
return 0;
}

在这个例子中,用一个右值引用rref绑定了一个临时对象。根据C++规则,这样做会延长临时对象的生命周期,使其与右值引用的生命周期相同。所以,在main函数结束前,这个临时对象都不会被销毁。这里没有发生拷贝,也没有移动,只是给临时对象起了一个别名。

BUT

看看到现在,还是用高层的概念去解释一个新的概念。如果一直在高层徘徊,就会产生很多疑问:临时对象是在内存中存储吗?int&&a=2, 2的地址不能被取到,那为什么用右值引用就能取了?那是不是意味着编译器在中间就是开了一个临时对象呢?又有说表达式结束,临时对象就会被销毁。那么如果还是按照刚刚那些高层概念解释,对象都被销毁了,右值引用还在引用什么呢?

还是下到汇编看看。终于在知乎找到了一篇文章,终于在汇编层面解答了我上面的问题:

https://zhuanlan.zhihu.com/p/389978619?share_code=IPKeoBHAwX3u&utm_psn=1960118829079307210

下面是答案:

int &&a=0;

其中a是一个右值引用。

这个汇编代码等价于一个左值引用(底层看就是指针常量)引用了一个匿名变量,此匿名变量在C++中不可见,但其实该变量存在于栈上。总结来说,右值引用的底层就是一个指针指向了一个匿名变量。那么如果对右值引用重新赋值修改,改的就是匿名变量的值。