捕获带有副作用的assert()

时间:2012-05-15 02:36:46

标签: c gcc assert

我们有几个中等大小的C代码库,可以接收来自具有各种经验级别的开发人员的提交。一些不那么有纪律的程序员提交assert()语句,副作用会导致断言被禁用。 E.g。

assert(function_that_should_always_be_called());

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

4 个答案:

答案 0 :(得分:7)

尽管这个问题已经收到了许多无用的答案,但我认为它在遗留代码库的上下文中有很多优点。

想象一下,多年来积累了很多断言,但由于没有使用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。与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-ar和{{ 1}}作为任何静态库的归档器/索引器。

此设置可以极大地减少我必须调查的断言数量。凭借最小的人力,我能够将断言清理干净。我仍然需要手动拒绝的误报是由于:

  • 虚拟函数调用
  • 非平凡的循环/递归(优化器无法证明它们是有限的)

此外,我还会得到一些确实包含副作用的断言,但它们是无害的或不重要的,例如:

  • 包含记录语句的函数
  • 缓存其结果的函数

答案 1 :(得分:3)

  

通过足够简单的控制流程,GCC应该可以确定您只调用纯函数。

如果它不是一个简单的控制流程,它将如何知道它是否纯净?


这样的事情可能是你最好的选择:

#ifdef NDEBUG
#define assert(s) do { (s); } while(false)
#else
// ...
#endif

将编译出几个表达式,包括__attribute__((pure))的函数。

最合乎逻辑的解决方案是审核您的代码并修复错误。

答案 2 :(得分:3)

即使GCC可以可靠地检测纯计算(这需要解决暂停问题),标志必须具有额外的神奇能力才能注意到非纯计算作为参数传递给本土的断言宏。扩展也无济于事 - 究竟应该做什么?

您的问题的解决方案是

  1. 聘请有能力的开发人员。
  2. 教育您的开发人员如何使用断言(以及其他内容)。
  3. 执行代码审核。
  4. 对可交付版本进行所有测试 - 如果断言在交付项中是关闭的,那么断言(function_that_should_always_be_called())与简单地省略function_that_should_always_be_called()没什么不同,这是一个应该在测试中捕获的明显错误。

答案 3 :(得分:0)

我不确定这对你所描述的应用程序是否足够,但是cppcheck会查找" assertWithSideEffect" s: http://cppcheck.sourceforge.net/devinfo/doxyoutput/checkassert_8cpp_source.html

这是编译时消息的样子: [assertWithSideEffect] myFile.cpp:42:警告:非纯函数:' myFunction'在assert语句中调用。从发布版本中删除断言语句,因此不会执行assert语句中的代码。如果在发布版本中也需要代码,那么这就是一个错误。

" Cppcheck是一个用于C / C ++代码的静态分析工具。与C / C ++编译器和许多其他分析工具不同,它不会检测代码中的语法错误。 Cppcheck主要检测编译器通常无法检测到的错误类型。目标是仅检测代码中的实际错误(即,具有零误报)。" http://cppcheck.sourceforge.net/