不得不吐槽下:大学有个别老师确实挺那啥的,可能自己也不是很懂吧,讲个右值引用遮遮掩掩的就是点不出它到底是个什么玩意儿。
最后还是我自己去查了些资料,并暂且形成了一些粗浅的理解。时间有限,具体的都集中在如下的代码之中:
#include <iostream>
void func(int* &n) {
std::cout << "left value reference is caught!" << std::endl;
std::cout << *n << std::endl;
}
void func(int* &&n) {
std::cout << "right value reference is caught!" << std::endl;
std::cout << *n << std::endl;
}
int main() {
int* ptr = new int(666);
int* &ref_left = ptr;
int* &&ref_right = std::move(ptr);
func(ptr);
// 无论是左值引用变量还是右值引用变量,
// 在当作实参传入函数的时候,都会被当作左值(因为它们作为变量可以直接被取地址)
// 因此下面两条代码都会输出left value reference is caught!
func(ref_left);
func(ref_right);
// new关键字会触发malloc并返回分配空间的首地址。
// 由于返回值没有被任何已声明的变量创建,因此其身份为一个右值。
// 在此情况下,由于函数func的重载支持接收右值,可以通过编译器编译。
// 调用func函数前编译器会在栈上分配一个c++代码不可见的"匿名变量",并将返回的void*指针值存入其中。
// 再将该"匿名变量"载入寄存器,传递给func以供调用。
// 这就是右值引用的本质,具体用visual studio的反汇编功能进行观测。
func(new int(888));
// std::move仅仅告诉编译器将一个左值当作右值来处理,在汇编层面不会有任何多余的操作
// 因此std::move的本质为c++层面的变量类型转换。
// 第一种情况,若传递给std::move的是一个非引用类型的变量,
// 从汇编层面来看,它会触发对变量的取地址操作,实现所谓"引用"的效果
// P.S: 从汇编层面来看,所谓"引用"只是指针操作的语法糖!!!
func(std::move(ptr));
// 第二种情况,若传递给std::move的是一个引用类型的变量
// 此时std::move仅仅是告诉编译器将传递给它的值作为一个右值进行看待,在汇编层面不会生成任何代码。
// 由于是引用类型的变量,因此执行如下两行代码的时候,
// 会将引用变量实际持有的被引用变量的地址传递给func函数,实现所谓"引用"的效果
func(std::move(ref_left));
func(std::move(ref_right));
}
当然,以上的代码只是从纯语法和汇编的角度揭示了编译器是如何处理右值引用和move语义的,并没有任何实际意义。
要处理形如func(new int(666))的函数调用,直接声明一个签名形如void func(int*)的函数就行了...
最近事比较多。下次有空的时候我再继续撰文,一点一点慢慢理清楚右值引用和move语义的一些实际应用场景:
-
支持移动语义 (Move Semantics) :在C++11之前,我们只能复制对象。但复制可能是代价昂贵的操作,特别是对于大型对象。而通过移动语义,我们可以简单地"转移"资源,而不是复制它,从而提高性能。
例如,考虑一个包含大型动态数组的
std::vector。复制一个这样的向量可能需要大量时间和内存。但通过移动,我们可以仅仅交换资源指针,这是一个常量时间操作。 -
完美转发 (Perfect Forwarding) :右值引用允许模板函数按其原始类型(lvalue或rvalue)转发其参数,这在模板编程和
std::forward中特别有用。 -
使得返回局部对象成为可能:在C++11之前,返回局部对象通常会导致额外的复制。但是现在,多亏了移动语义,这种复制可以被避免。
-
使得某些库设计和优化成为可能:例如
std::unique_ptr就是一个利用右值引用来确保对象唯一拥有的智能指针。
参考资料: