尽管这个问题已经收到许多无益的非答案,但我认为它在遗留代码库的背景下有很多优点。
想象一下,多年来积累了许多断言,但由于没有使用NDEBUG进行构建/测试的习惯,因此一些副作用已经流入断言中,现在您不敢断言断言了。
您可以打开NDEBUG并检测测试套件中的某些测试失败,但将测试失败与“有效”断言关联起来并不容易,因为它可能距离检测失败的地方非常远。即使是具有良好覆盖范围的测试套件也不能完全信任。
您可以对代码中的所有断言进行代码审查,但这可能是很多工作并容易出现人为错误。如果某些静态分析已经可以消除所有可以证明不会出现副作用的断言,并且只需要调查那些不能保证其缺席的情况,那将会好得多。
下面介绍如何使用编译器的优化器进行这种静态分析。假设你通过组织来代替assert
宏的定义:
extern int not_supposed_to_survive;
#define assert(expr) ((void)(not_supposed_to_survive || (expr)))
如果expr
有任何副作用,效果的执行是以全局变量not_supposed_to_survive
的价值条件。但是如果expr
没有任何副作用,那么全局变量的值无关紧要(注意expr
结果将被丢弃)。 好的优化器知道这一点,并将消除全局变量not_supposed_to_survive
的负载,因此该变量的名称。
如果我们的程序没有包含符号not_supposed_to_survive
的定义,那么当负载没有被消除时,我们会得到一个链接错误,我们可以使用它来检测潜在的有效断言。
E.g.与海湾合作委员会4.8:
int g;
int foo() { return ++g; }
int main() {
assert(foo());
return 0;
}
gcc -O2 assert_effect.c
/tmp/ccunynya.o: In function `main':
assert_effect.c:(.text.startup+0x2): undefined reference to `not_supposed_to_survive'
collect2: error: ld returned 1 exit status
编译器帮助我找到一个可疑的断言!另一方面,如果我将++g
替换为g+1
,则链接错误消失,我不必调查。事实上,这个断言是保证无害的。
当然,可证明无副作用的概念受限于优化程序“可以看到”的内容。为了更精确的分析,我建议使用链接时间优化(gcc -flto
)来分析各个编译单元。
更新:我将此应用于使用gcc 5.3的现实生活C++代码库。要使用链接时优化,基本上使用gcc -flto -g
作为编译器/链接器(编译器/链接器上的-g
选项以获取链接错误的行参考),并使用gcc-ar
和gcc-ranlib
作为任何静态库的存档器/索引器。
这种设置可以极大地减少我不得不调查的断言的数量。用最少的人力,我能够使断言变得清晰。误报,我仍然不得不手动调低的原因是:
- 虚函数调用
- 非平凡循环/递归(其中优化不能证明他们是有限的)
此外,我还会得到一些确实含有副作用的断言,但它们无害或不重要,例如:
不,GCC不检查功能是否有副作用。 –
也许需要在提交之前进行代码审查。 –
为了澄清我的问题,我们没有资源对这些存储库中的某些存储库执行手动代码审查。对于我们的高优先级,我们做代码审查。我一直在寻找自动化的东西,这将使我们不得不为这些微不足道的错误承诺提供回扣。 – Matthew