C++ 左值、右值与 std::move
2025-03-30

“左值”与“左值引用”

下面代码中,a 属于自己的内存空间,它是左值。而 1020 没有地址属性,只是一个常量,都不能放在常量左边,它们都是右值。

1
2
3
4
5
6
7
int a = 10; // a 就是地址别名
// 汇编:把 10 放到了变量 a 里面
00007FF7AF111E9C mov dwoard ptr [a], 0Ah

a = 20;
// 汇编:把 20 放到了变量 a 里面
00007FF7AF111EA3 mov dwoard ptr [a], 14h

下面的 b 就是左值引用。引用是变量的别名,引用通过指针来实现

1
2
3
4
5
int a = 10;
00007FF7AF111EBE mov dword ptr [a], 0Ah
int& b = a;
00007FF7AF111EC5 lea rax, [a] // 记下 a 的地址
00007FF7AF111EC9 mov dword ptr [b], rax // 将 a 的地址存入 b 地址

此时我们将 b 修改,类似于指针会帮我们做一次 * 解引用,直接修改了 a

1
2
3
b = 20;
00007FF7AF111ECD mov rax, qword ptr [b]
00007FF7AF111ED1 mov dword ptr [raw], 14h

“右值”与“右值引用”

右值一般没有地址。我们可以这么理解,右值引用就是右值的临时变量。

1
2
3
4
5
6
7
const int&& c = 20;
// 20 字面常量没地址,存到了临时栈上空间
00007FF7AF111ED7 mov dword ptr [rbp+64h], 14h
// 此时右值 20 就有了临时地址
00007FF7AF111EDE lea rax, [rbp+64h]
// 此时再让指针指向 20 的临时地址
00007FF7AF111EE2 mov qword ptr [c], rax

move 移动语意

std::move 的作用是将左值转换为右值。

上面的代码中,1020 都是纯右值,在汇编里是没有任何地址属性。我们来看看,如何使用 std::move 将左值转换为右值,在汇编中是怎么样的。

1
2
3
const int&& c = std::move(a);
00007FF7AF111E77 lea rax, [a] // 记下 a 的地址
00007FF7AF111E7B mov dword ptr [c], rax // 将 a 的地址存入 c 地址

我们发现,这和左值引用一模一样!左值引用是:

1
2
3
int& b = a;
00007FF7AF111EC5 lea rax, [a] // 记下 a 的地址
00007FF7AF111EC9 mov dword ptr [b], rax // 将 a 的地址存入 b 地址

更准确来说 std::move 仅仅是将包裹着 movea 变成了右值,而变量 c 依旧是指向 a 的引用。其中 a 叫做将亡值。唯一的目的:c 在传递或者赋值时,触发移动构造,避免深拷贝。下面是一个例子:

1
2
3
4
5
std::string str1 = "Helloooooooooooooooooooooooo";
std::string str2 = str1; // 触发拷贝构造,是深拷贝,如果 str1 很长,会浪费很多资源

std::string str1 = "Helloooooooooooooooooooooooo";
std::string str2 = std::move(str1); // 触发移动构造

不过要注意,std::move 之后,在语意上我们将原来的左值给“移动”了,再去访问原来的对象就不符合移动语意。下面的例子中进行了一次非法访问,如果是更复杂的变量,可能会出现 core dump 的情况。

1
2
3
4
std::cout << "str1: " << str1 << std::endl; 
std::cout << "str2: " << str2 << std::endl;
// str1:
// str2: Helloooooooooooooooooooooooo