本工具可以在 OI 系列竞赛中使用(只要你的省份提供了 NOI Linux)。

Sanitizer

在编译时通过加入开关 -fsanitize=undefined 来检查未定义行为。
只有 Clang 和 Linux 环境下的GCC可以使用这个功能。(所以立马换到linux吧)

先来看一个例子:

1
2
3
4
5
6
7
8
#include<bits/stdc++.h>
using namespace std;

int main(){
int a = 1e9;
printf("%d\n",a*a);
return 0;
}

显然这段代码会发生有符号整数溢出,这是一个未定义行为。输入 g++ test2.cpp -o test2 -Wall -fsanitize=undefined。进行运行,得到结果:

$ ./test2
test2.cpp:6:11: runtime error: signed integer overflow: 1000000000 * 1000000000 cannot be represented in type 'int'
-1486618624

我们发现,-fsanitize 会将错误输出到 stderr 流中,并且会给出行号以及具体执行的值。在遇到超级多整数乘法的时候,你只知道最后是一个负数,说明你溢出了,但是你很难找到到底是哪一行溢出了。这样就可以快速找出溢出的行。

常见未定义行为

有符号整数溢出

这是一个最常见的错误。无论是你少开了一个 long long,还是忘记答案要用 long long 存,都会溢出,而有时候溢出的结果还是一个正数,这就使发觉这个错误变得十分困难。

好在只要一直开着-fsanitize=undefined,就可以随时将溢出的情况汇报。

样例如上。

访问栈上未初始化的变量

谁没写过这样的代码呢?

1
2
3
4
5
6
7
8
9
10
11
12
#include<bits/stdc++.h>
using namespace std;

int main(){
int ans;
for(int i=0;i<5;i++){
int x;scanf("%d",&x);
if(x>ans)ans=x;
}
printf("%d\n",ans);
return 0;
}

这也是一个未定义行为,但很可惜,-fsanitize=undefined 检测不到这个错误。
但是在 -O2 条件下的 -Wall 可以检测到这个错误。

$ g++ test2.cpp -o test2 -Wall -fsanitize=undefined -O2
test2.cpp: In function ‘int main()’:
test2.cpp:7:20: warning: ignoring return value of ‘int scanf(const char*, ...)’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
    7 |         int x;scanf("%d",&x);
      |               ~~~~~^~~~~~~~~
test2.cpp:5:9: warning: ‘ans’ may be used uninitialized in this function [-Wmaybe-uninitialized]
    5 |     int ans;
      |         ^~~

数组越界

直白的错误。

1
2
3
4
5
6
7
8
9
10
11
#include<bits/stdc++.h>
using namespace std;

int a[5];

int main(){
for(int i=0;i<10;i++){
a[i]=i;
}
return 0;
}

-fsanitize=undefined 可以准确报出行号以及访问的下标。

$ ./test2
test2.cpp:8:12: runtime error: index 5 out of bounds for type 'int [5]'
test2.cpp:8:13: runtime error: store to address 0x559f3230a0d4 with insufficient space for an object of type 'int'
0x559f3230a0d4: note: pointer points here
  04 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
              ^ 

对性能的影响

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include<bits/stdc++.h>
using namespace std;

using ll=long long;
using ull=unsigned long long;
using pii=pair<int,int>;
#define begend(x) x.begin(),x.end()
#define mem0(x) memset(x,0,sizeof(x))
#define YES puts("Yes")
#define NO puts("No")

int read(){
int f=1,x=0;char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){x=x*10+c-'0';c=getchar();}
return x*f;
}

int t,n,m;
int bad[105];

int remain;
ll ans;

void dfs(int dep){
if(remain==0)return;
if(dep==t-1){
for(int i=0;i<m;i++){
if((remain&bad[i])==bad[i]){
return;
}
}

ans++;
return;
}

for(int S0=remain;S0;S0=(S0-1)&remain){

bool ok = 1;
for(int i=0;i<m;i++){
if((S0&bad[i])==bad[i]){
ok=0;
break;
}
}
if(!ok)continue;

remain &= ~S0;
dfs(dep+1);
remain |= S0;
}
}

int main(){
n=read(),t=read(),m=read();
for(int i=0;i<m;i++){
int a=read(),b=read();
bad[i]= (1<<a)|(1<<b);
}

for(int i=1;i<=n;i++)remain|=(1<<i);

dfs(0);

for(int i=2;i<=t;i++)ans/=i;

printf("%lld\n",ans);
return 0;
}

这里有一份爆搜代码。我们尝试考察它对 10 10 0 这个输入的用时。
测试环境 CPU: 12th Gen Intel i5-12500H (16) @ 3.110GHz

用时如下(未做多次实验取平均值):

用时
无开关 0.901s
-O2 0.574s
-fsanitize=undefined 0.977s
-fsanitize=undefined -O2 0.593s

发现在递归场景下,似乎性能影响不大。

再来看一个大量数组操作的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<bits/stdc++.h>
using namespace std;

const int N = 1e8;
unsigned long long fib[N];

int main(){
fib[0]=1,fib[1]=1;
for(int i=2;i<N;i++){
fib[i]=fib[i-1]+fib[i-2];
}
printf("%llu\n",fib[N-1]);
return 0;
}

用时如下(未做多次实验取平均值):

用时
无开关 0.376s
-O2 0.249s
-fsanitize=undefined 0.563s
-fsanitize=undefined -O2 0.328s

可以看到性能有略微损失,这是动态差错不可避免的。性能损失在可以接受的范围内。

综上,建议你在编写每一份代码时,都在编译命令添加上 -fsanitize=undefined