首页 > 其他分享 >记一个难以发现的 UB

记一个难以发现的 UB

时间:2023-01-03 22:46:49浏览次数:37  
标签:发现 int 代码 back 编译器 solve push 难以 UB

观察以下代码:

vector<int> X, Y, A, val;
inline int ls(int p) { return p << 1; }
inline int rs(int p) { return p << 1 | 1; }
int solve(int i, int l, int r) {
    if (l == r) return val[i] = A[l];
    int mid = (l + r) >> 1, p = X.size();
    X.push_back(0), Y.push_back(0);
    X[p] = solve(ls(i), l, mid);
    Y[p] = solve(rs(i), mid + 1, r);
    // do something
    return val[i];
}

这是一份标准的线段树分治代码,其中数组 \(A\) 是给定的,\(val\) 在 \(solve\) 函数调用之前已经分配好了内存,而 \(X\) 和 \(Y\) 的内存空间则是动态分配的。

当我在本地测试完整的代码时,不会出现任何的异常。当我将代码提交到学校的 OJ 上时,却发现输出的结果不符合预期,而且对于同样的输入,输出却和本地有所出入。

经过艰难的排查,我最终发现问题出现在了 \(solve\) 函数中,即上述代码的第 \(8\) 至 \(9\) 行。我尝试将这两行替换为下面的代码:

int lp = solve(ls(i), l, mid);
X[p] = lp;
int rp = solve(rs(i), mid + 1, r);
Y[p] = rp;

这时 \(X[p]\) 与 \(Y[p]\) 的值就从错误的 \(0\) 变成了正确的答案。

我不禁陷入沉思,为何看似逻辑完全相同的代码,产生的效果却大相径庭?直到我发现第 \(7\) 行代码中的操作:

X.push_back(0), Y.push_back(0);

有没有可能,在第 \(8\) 行和第 \(9\) 行的赋值过程中,编译器先对等号左边的表达式进行计算,得到 \(X[p]\) 和 \(Y[p]\) 的左值引用,然后再计算了等号右边的表达式,调用了 \(solve\) 函数呢?

这样一切就解释得通了,\(X[p]\) 和 \(Y[p]\) 的引用先被取出,然后在递归调用 \(solve\) 函数的过程中,执行到了第 \(7\) 行的 \(push\_back\) 函数,使得 \(vector\) 重新分配了堆空间,导致 \(X[p]\) 和 \(Y[p]\) 的引用失效。于是,在赋值的过程中,我们对一个已经被释放掉的空间进行了修改,且不说有没有访问到不该访问的位置,当前 \(vector\) 中真实的 \(X[p]\) 和 \(Y[p]\) 也没能被赋为正确的值。

现在我们弄清楚发生 UB 的过程了。在这之后,我又进行了一些测试,目的在于弄清楚产生两种不同情况的本质原因。继续观察以下代码:

#include <bits/stdc++.h>
using namespace std;
int func1() {
    cout << "func1" << endl;
    return 1;
}
int func2() {
    cout << "func2" << endl;
    return 2;
}
int func3() {
    cout << "func3" << endl;
    return 3;
}
struct node {
    int arr[100];
    int& operator[](int i) {
        func1();
        return arr[i];
    }
};
int main() {
    node a;
    (a[0] = func2()) = func3();
    return 0;
}

当我使用 g++ 作为编译器,输出结果如下:

func1
func2
func3

当我使用 clang 作为编译器,输出结果如下:

func3
func2
func1

归根结底,产生这两种区别的原因还是在于编译器的实现。从上面的例子可以看出,g++ 在执行赋值语句的过程中,会从左往右进行运算,而 clang 则是从右往左。

在我的本机上,常用的编译器是 apple-clang,因此上文中线段树分治的代码从右往左执行赋值操作,不会产生引用失效的问题。而学校 OJ 的默认编译器为 g++,自然就出现与预期相违的情况了。

个人认为,对于这两种执行顺序,应当是从右往左更加符合正常人的逻辑,毕竟如 A = B = C 这样的连续赋值语句也是从右往左执行的。

总而言之,为了不触发此类未定义行为,在写代码时还需要多注意一下。对于本文开头的例子,最好还是在调用 \(solve\) 函数之前先对 \(X\) 和 \(Y\) 的内存空间进行 \(reserve\),这样就不会在 \(push\_back\) 时出现引用失效的问题了。

标签:发现,int,代码,back,编译器,solve,push,难以,UB
From: https://www.cnblogs.com/yaoxi-std/p/17023579.html

相关文章

  • SqlServer的substring用法
    SUBSTRING(expression,start,length) 参数expression字符串、二进制字符串、文本、图像、列或包含列的表达式。请勿使用包含聚合函数的表达式。 start整数......
  • ruby on rails 常用命令汇总
    Rails常用的命令汇总:1、railsnewrails_4.2.17_newmyapprailsnewdemo--skip-test-unitorrailsnewdemo-Trailsnewdemo-dmysql-Trailsgeneraterspec:......
  • git/github初级运用自如 (good)
    三.设置用户信息这一步不是很重要,貌似不设置也行,但github官方步骤中有,所以这里也提一下。在git中设置用户名,邮箱$gitconfig--globaluser.name"defnngj"//给自己起个......
  • kubernetes简单使用教程(一)
    一、命名空间作用:用来隔离资源添加删除命名空间[root@k8s-master01~]#kubectlcreatenshellonamespace/hellocreated[root@k8s-master01~]#kubectlgetnsNAME......
  • ubuntu网络配置
    在完成基础安装的ubuntu虚拟机上配置网络,实现:(1)能ping通本机(2)能访问外网(3)配置静态ip1,编辑虚拟机设置-->桥接模式(复制。。。)-->确定2,编辑-->编辑虚拟机设置 更改设......
  • substantial
    substantial今天看到非常好的一个单词,substantial。这个单词是在看Python的Django文档看到的,意思是大量的,价值巨大的;牢固的,结实的...(饭菜)丰盛的;重要的,真实的;有地位的,......
  • github慢,win,linux怎么解决
    推荐两种方法解决github访问慢的方法(win,linux通用)方法11.访问https://www.ipaddress.com2.获取下面网址iphttp://github.global.ssl.fastly.nethttp://github.co......
  • Ubuntu扩容gparted
    今天在使用ubuntu的时候弹出系统的磁盘空间不足,导致apt-getinstall一些工具都失败了。 进入虚拟机设置-硬盘-扩展硬盘容量 安装gparted 使用$sudoapt-getinstallg......
  • ubuntu22.04启用sshd远程
    1.系统升级ubuntu有一个很麻烦的特性,就是新装的系统需要先更新系统:sudoapt-getupdate-y2.安装openssh-server【sshd】sudoaptinstall openssh-server 3.开......
  • 打开sublime text3 弹出错误提示 Error trying to parse settings: Expected value in
    问题:打开sublimetext3弹出错误提示Errortryingtoparsesettings:ExpectedvalueinPackages\UserJSONsublime-settings:13:17原因:一般是配置文件出现语法错误,可根......