记一个难以发现的 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\) 时出现引用失效的问题了。
记一个难以发现的 UB的更多相关文章
- 记一个社交APP的开发过程——基础架构选型(转自一位大哥)
记一个社交APP的开发过程——基础架构选型 目录[-] 基本产品形态 技术选型 最近两周在忙于开发一个社交App,因为之前做过一点儿社交方面的东西,就被拉去做API后端了,一个人头一次完整的去搭这么一 ...
- Entity Framework学习笔记——记一个错误解决方式及思路
继续之前设定的学习目标前,先来一篇小小的外篇.按照第一篇里的配置方式配置好的工程前两天还能正常工作,昨天却突然无法通过Add-Migration命令进行数据库的升级.错误信息如下: System.Da ...
- hosts文件的一个小发现
今天才发现原来同一个ip可以在hosts文件中配置多个域名.之间老是换一个网站就改一下,现在终于不用这么麻烦了 127.0.0.1 gg.pclady.com.cn 127.0.0.1 gg.pcon ...
- 记一个界面刷新相关的Bug
今天遇到一个比较有意思的bug, 这里简单记录下. Bug的症状是通过拖拉边框把我们客户端主窗口拖小之后,再最大化,会发现窗口显示有问题, 看起来像是刷新问题, 有些地方显示的不对了. 这里要说明的是 ...
- 记一个同时支持模糊匹配和静态推导的Atom语法补全插件的开发过程: 序
简介 过去的一周,都睡的很晚,终于做出了Atom上的APICloud语法提示与补全插件:apicloud_autocomplete.个中滋味,感觉还是有必要记录下来的.代码基于 GPL-3.0 开源, ...
- 记一个dynamic的坑
创建一个控制台程序和一个类库, 在控制台创建一个匿名对象,然后再在类库中访问它,代码如下: namespace ConsoleApplication1 { class Program { static ...
- 记一个奇怪的python异常处理过程
我的一个程序, 总是在退出时报异常, Exception TypeError: "'NoneType' object is not callable" in <functio ...
- 记一个python+sqlalchemy+tornado的一个高并发下,产生重复记录的bug
场景:在用户通过支付通道支付完成返回时,发现我收到的处理数据记录中有两条同样的数据记录, 也就是同一笔钱,我数据库中记为了两条一样的记录. tornado端代码 from tornado import ...
- 彷徨中的成长-记一个文科生的IT成长过程
纠结了许久,要不要写这篇文章,然而最终还是写了.就权当总结与呻吟吧..当然,呻吟最开始还是发在自己的站点的,忍不住手贱,还是想发博客园. 1 剧透 人算不如天算:时隔多年,我竟然搞起了前端. 2 发端 ...
- 记一个菜鸟在Linux上部署Tomcat的随笔
以前都只是在园子里找各种资料.文档.各种抱大腿,今天是第一次进园子里来添砖加瓦,实话说,都不知道整些啥东西上来,就把自己在Linux上搭建Tomcat的过程记录下来,人笨,请各位大虾们勿喷. 虽然做开 ...
随机推荐
- Unity破窗游戏制作(简易版)
Unity破窗游戏制作(简易版) 参考:"对不起,我选择摸鱼"-<扫雷>小游戏开发实战,算法.源代码,基于Unity3D开发 - 掘金 (juejin.cn) 到&qu ...
- python基础爬虫,翻译爬虫,小说爬虫
基础爬虫: # -*- coding: utf-8 -*- import requests url = 'https://www.baidu.com' # 注释1 headers = { # 注释2 ...
- Redis 常见问题-缓存穿透
问题描述: * 针对 DB 中不存在的数据源,每次请求缓存和数据库都不存在 造成后果: * 应用服务器压力变大 * Redis 命中率大幅度降低 * `数据库压力巨增甚至 down 掉`* 该现象对于 ...
- 【React】学习笔记(二)——组件的生命周期、React脚手架使用
原教程视频:ttps://www.bilibili.com/video/BV1wy4y1D7JT?p=2&spm_id_from=pageDriver 目录 一.组件的生命周期 1.1.生命周 ...
- pta第一次博客
目录 pta第一次博客 1.前言 2.设计与分析 第二次作业第二题 第三次作业第一题 第三次作业第二题 第三次作业第三题 3.踩坑心得: 4.改进建议 5.总结 pta第一次博客 1.前言 这三次pt ...
- 【YOLOv5】手把手教你使用LabVIEW ONNX Runtime部署 TensorRT加速,实现YOLOv5实时物体识别(含源码)
前言 上一篇博客给大家介绍了LabVIEW开放神经网络交互工具包[ONNX],今天我们就一起来看一下如何使用LabVIEW开放神经网络交互工具包实现TensorRT加速YOLOv5. 以下是YOLOv ...
- VBA工程设置密码
VBA 工程设置密码 Alt + F11,进入程序界面: 工具---> VBAProject属性---> 保护---> 查看时锁定工程前打勾,并在下面的密码区输入密码.
- 05 uniapp/微信小程序 项目day05
一.登录与支付 1.1 登录 1.1.1 条件判断 当我们点击结算应当进行条件判断 第一个如果没有勾选商品 第二个是没选择地址 第三个是未登录 1.1.2 页面布局 应该有两个页面,一个点击登录, ...
- 【lwip】12-一文解决TCP原理
目录 前言 12.1 TCP协议简介 12.2 TCP相关的一些概念词 12.2.1 MSL 12.2.2 MSS 12.3 TCP工作特性 12.3.1 面向连接 12.3.2 全双工通信 12.3 ...
- 【题解】CF356A Knight Tournament
题面传送门 本蒟蒻想练习一下并查集,所以是找并查集标签来这里的.写题解加深理解. 解决思路 自然,看到区间修改之类很容易想到线段树,但本蒟蒻线段树会写挂,所以这里就讲比较简单的并查集思路. 并查集的核 ...