我们有几个中等大小的C代码库,它们接收来自具有各种经验级别的开发人员的提交。一些纪律性不强的程序员提交assert()语句时会产生副作用,导致禁用断言时出现错误。例如。

assert(function_that_should_always_be_called());

我们已经使用了自己的assert()实现,但是使用定义的NDEBUG评估表达式将导致不可接受的性能降低。我们是否可以传递gcc扩展或标志来触发编译时警告/错误?使用足够简单的控制流,gcc应该可以确定您只调用纯函数。

最佳答案

尽管这个问题得到了许多无益的不回答,但我认为它在遗留代码基础的上下文中有很多优点。
假设多年来积累了很多断言,但是由于没有使用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的定义,当负载没有消除时,我们将得到一个链接错误,我们可以使用它来检测潜在的有效断言。
如GCC 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-argcc-ranlib作为任何静态库的存档器/索引器。
这种设置可以极大地减少我必须调查的断言的数量。用最少的人力,我就能把断言弄清楚。我仍然需要手动拒绝的误报是由于:
虚拟函数调用
非平凡循环/递归(优化器无法证明它们是有限的)
此外,我也会得到一些确实包含副作用的断言,但它们是无害的或不重要的,例如:
包含日志语句的函数
缓存结果的函数

关于c - 捕获有副作用的assert(),我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/10593492/

10-17 01:36