浅谈 C++ const
danzong_dan · · 科技·工程
引入
分别考虑以下代码:
#include <bits/stdc++.h>
int main() {
const int a = 1;
const_cast<int &>(a) = 42;
std::printf("%d\n", a);
}
#include <bits/stdc++.h>
int main() {
const int a = std::rand();
const_cast<int &>(a) = 42;
std::printf("%d\n", a);
}
请问两次代码分别会输出什么?
运行后不难发现,前者会输出 1,后者则输出 42。事实上,两者逻辑几乎一致:
- 定义一个常量
a并初始化 - 强制修改
a的值 - 输出
a
那为什么行为上存在差异呢?
汇编分析
我们通过 Compiler Explorer 查看二者的汇编代码(省略部分代码),如下:
main:
; ...
lea rdi, [rip + .L.str] ; 传递printf第一个参数
mov esi, 1 ; 传递printf的第二个参数
xor eax, eax ; 将eax寄存器清零,便于printf调用
call printf ; 调用printf
; 以上代码相当于printf("%d\n", 1);
; ...
.L.str:
.asciz "%d\n"
main:
; ...
call rand ; 调用 rand 函数
lea rdi, [rip + .L.str] ; 传递 printf 第一个参数
mov esi, 42 ; 传递 printf 的第二个参数
xor eax, eax ; 将 eax 寄存器清零,便于 printf 调用
call printf ; 调用 printf
; 以上代码相当于 printf("%d\n", 42);
; ...
.L.str:
.asciz "%d\n" ; 定义格式化字符串
观察到,编译器忽略了 a 的内存分配,并直接使用 Magic Number 作为 A 的值。
符号表替换
我们先分析代码 A。通过查阅资料可知,编译器会进行符号表替换优化,具体来说,会将所有编译期常量替换为 Magic Number,如以下代码:
const int a = 114514;
int b[a];
会被优化为
int b[114514];
这一优化发生在 AST 阶段,位于预处理之后,汇编之前。那么回到刚才的代码,
#include <bits/stdc++.h>
int main() {
const int a = 1;
const_cast<int &>(a) = 42;
std::printf("%d\n", a);
}
显然,按照刚才的逻辑,程序会被优化成这样:
#include <bits/stdc++.h>
int main() {
const int a = 1;
const_cast<int &>(a) = 42;
std::printf("%d\n", 1);
}
那么,此时显然
const int a = 1;
const_cast<int &>(a) = 42;
已经没有任何意义,那么编译器会根据 as-if 原则(**编译器可以自由地改变程序,只要可观察行为与原始程序一致。**),这段代码就会被优化。
最终被优化为:
#include <bits/stdc++.h>
int main() {
std::printf("%d\n", 1);
}
代码 B 分析
接下来考虑代码 B。我们知道,符号表替换适用于编译期常量,显然对于代码 B 不适用。接下来,编译器会考虑将变量 a 分配至 .rodata 段(只读数据段) 。然而很不幸,上述方法不适用于局部变量。因此,编译器只能像普通变量一样处理 a,只不过在编译器进行检查。
但是,const_cast 会拒绝编译器检查,相当于告诉编译器“我保证这段代码是安全的”。于是,编译器检查通过后,源代码
#include <bits/stdc++.h>
int main() {
const int a = std::rand();
const_cast<int &>(a) = 42;
std::printf("%d\n", a);
}
会被处理为像这样:
#include <bits/stdc++.h>
int main() {
int a = std::rand();
a = 42;
std::printf("%d\n", a);
}
此时,编译器会进行数据流分析最后发现:
唯一一次获取 a 的值,即 printf 时,a 的值是确定的,为 42。因此,编译器会认为 a 是没有意义的,优化成这样:
#include <bits/stdc++.h>
int main() {
std::rand();
std::printf("%d\n", 42);
}
请注意,此处的 std::rand 是有副作用的,也就是说,执行 std::rand 会改变程序状态。因此,编译器不会删除 std::rand 的调用,但是会忽略其返回值。
写在最后
永远不要尝试修改一个常量!这在 C++ 中是未定义行为,也就是说,这种操作的结果是不确定的,编译器可以对未定义行为进行任何处理。上述分析只是当前主流编译器的普遍优化方法。
参考
- ISO/IEC 14882:2024
- https://godbolt.org/
- https://chat.deepseek.com/