惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
S
SegmentFault 最新的问题
V
Visual Studio Blog
人人都是产品经理
人人都是产品经理
阮一峰的网络日志
阮一峰的网络日志
腾讯CDC
Stack Overflow Blog
Stack Overflow Blog
博客园 - 【当耐特】
Recent Announcements
Recent Announcements
I
InfoQ
U
Unit 42
博客园_首页
GbyAI
GbyAI
Hugging Face - Blog
Hugging Face - Blog
罗磊的独立博客
博客园 - 叶小钗
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
D
DataBreaches.Net
aimingoo的专栏
aimingoo的专栏
月光博客
月光博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 聂微东
T
Tailwind CSS Blog
量子位

Shiroha白羽的博客

Golang 踩坑 —— interface 为参数的时候传 nil 指针 Codeforces Round 925 (Div. 3) Codeforces Round 924 (Div. 2) Codeforces Round 923 (Div. 3) Codeforces Round 922 (Div. 2) Codeforces Round 921 (Div. 2) Educational Codeforces Round 161 (Rated for Div. 2) Codeforces Round 920 (Div. 3) Codeforces Round 919 (Div. 2) Hello 2024 Good Bye 2023 Codeforces Round 918 (Div. 4) 个人备份的常用 macOS 清理命令 Codeforces Round 917 (Div. 2) Pinely Round 3 (Div. 1 + Div. 2) Educational Codeforces Round 160 (Rated for Div. 2) Codeforces Round 915 (Div. 2) Codeforces Round 914 (Div. 2) Codeforces Round 913 (Div. 3) Educational Codeforces Round 159 (Rated for Div. 2) Codeforces Round 912 (Div. 2) Codeforces Round 911 (Div. 2) CodeTON Round 7 (Div. 1 + Div. 2, Rated, Prizes!) Educational Codeforces Round 158 (Rated for Div. 2) Codeforces Round 910 (Div. 2) Codeforces Round 909 (Div. 3) Codeforces Round 908 (Div. 2) Educational Codeforces Round 157 (Rated for Div. 2) C++自定义的字面量 Codeforces Round 907 (Div. 2)
左值-右值-将亡值
Shiroha · 2023-09-03 · via Shiroha白羽的博客

最初概念

如何确定一个值是左值还是右值?
通常有一个比较简单的判断方案:有地址的值被称为左值,没有地址的值称为右值

但是事实好像并非如此,特别是写了一些相关代码的时候,比如下面的这段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int f(int &a) {
return 1;
}

int f(int &&a) {
return 2;
}

void solve() {
int a = 1;
int &b = a;
int &&c = 1;

cout << f(1) << endl; // 2
cout << f(a) << endl; // 1
cout << f(b) << endl; // 1
cout << f(c) << endl; // 1
}

对应的输出结果也写在每一行的后面了,这似乎有一些意料之外的情况

  • 第一行,一个单独的数字 1,很明显的确实是一个右值,符合预期
  • 第二行,变量 a 明显也是一个合情合理的左值,那么也是符合预期的
  • 接下来第三行,变量 b 作为 a 的一个引用,那毫无意义也是一个左值(b 只是引用了 a 的值,实际上仍然是 a 本身),符合预期
  • 但是第四行,却让人摸不着头脑,明明 c 是一个明确的右值引用,为什么也是一个 1

这似乎表明了,c 是一个合法的左值,而非右值

尝试做一些看起来非法的操作

1
2
3
int &&c = 1;
c += 10;
cout << c << endl; // 11

看起来非常的合法合理,就像是一个活灵活现的左值,而并非它类型那样描述的右值。即然是左值,那么必然有地址,输出看看

1
2
3
cout << &a << endl; // 0x7fff1ba1f724
cout << &b << endl; // 0x7fff1ba1f724
cout << &c << endl; // 0x7fff1ba1f72c

从上面的数字可以看出来,c 确实是在栈上,即拥有一个合理合法的地址,这是发生了什么?

调查

如果把上述的代码改成汇编语言后,再看看结果

  • 汇编前
1
2
3
4
5
6
int main() {
int a = 1;
int &b = a;
int &&c = 1;
c += 10;
}
  • 汇编结果(仅摘录核心段)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp ; 以上均为函数定义需要的一些基本操作,例如记录栈位置等,忽略
movq %fs:40, %rax ; 设置 canary 值,用于检测 stack overflow 现象
movq %rax, -8(%rbp) ; 将 canary 值保存到栈的前 8 个字节中
xorl %eax, %eax ; 任何值 xor 自己必定为 0,此处相当于清理 eax 寄存器
movl $1, -32(%rbp) ; 将 1 存储到 28-32 这几个字节中(int 占用 4 个字节)【变量 a】
leaq -32(%rbp), %rax ; 将【a】的地址拷贝到 rax
movq %rax, -24(%rbp) ; 将【a】的地址保存到 16-24 这几个字节中(64bit 上占用 8 个字节)【变量 b】
movl $1, %eax ; 将值 1 写入 eax
movl %eax, -28(%rbp) ; 将 eax 的值写入 24-28 这几个字节中【未知变量】
leaq -28(%rbp), %rax ; 将【未知变量】的地址拷贝到 rax
movq %rax, -16(%rbp) ; 将【未知变量】的地址写入到 8-16 这几个字节中【变量 c】
movq -16(%rbp), %rax ; 再读取【变量 c】的到 rax
movl (%rax), %eax ; 将【变量 c】认为是一个地址,取出此地址中的值并写入到 eax 中
leal 10(%rax), %edx ; edx = rax + 10
movq -16(%rbp), %rax ; 将【变量 c】的值拷贝到 rax 中
movl %edx, (%rax) ; 将 edx 的结果保存到 rax 对应的值的地址中(即写入【变量 c】作为地址所在的位置)
movl $0, %eax ; 清空 eax
movq -8(%rbp), %rax ; 取出 canary 值
xorq %fs:40, %rax
je .L3
call __stack_chk_fail@PLT

可以注意到,对于引用而言,汇编仍然使用的是指针来解决,所以可以看到变量 b 记录下的是 a 的指针,而非真正的给 a 做了一个别名。而 c 也是一个指针,指向了一个未知的变量。这似乎就是我们寻找的答案

从内存本身而言,任何值都可以认为是左值,因为一个值存在,则必定存在具体的地址,即使它是作为常量的方式写在代码中,那起码它也应该存在于代码段,“存在即有地址”

但是对于这种在代码段“有地址”的值,又违背了代码段不可修改的原则,而具体操作的时候又未免会使用到这些值,这个时候,编译器会将代码段的这个值拷贝到栈空间,然后将其再赋给具体的对象,这个拷贝过来的值,像是一个右值,同时又具有着左值的特点,更确切的说,它属于“将亡值(xvalue)”。

将亡值

value-type

其中,lvalue 和 rvalue 就是我们一般认为上的左值和右值,而 glvalue 则是包含了将亡值的泛左值,而 prvalue 则是指那些纯右值,也就是那些在代码段里的值

将亡值则表示一种中间变量,例如使用了纯右值的时候,或者隐式类型转化,或者函数的返回值,这些都是将亡值充当的角色。实际上他们都有确切的栈上地址。

但是将亡值本身的含义是一个临时存在的变量,终是不可久留,这也就意味着编译器通常会限制对将亡值进行左值引用的方式。例如

1
double &x = (double)1;

此时编译器的报错是:Non-const lvalue reference to type 'double' cannot bind to a temporary of type 'double',即无法通过一个非常量的左值引用指向一个将亡值。而当你改成 const double &x = (double)1; 后,程序又可以通过编译了。这也说明了编译器实际上只是在做一些安全性的检查,并没有真正限制修改将亡值,甚至可以将将亡值变成长期存在的栈上的值(例如一开始的程序)