18

我们有几个中等大小的 C 代码库,可以接收来自具有各种经验水平的开发人员的提交。一些缺乏纪律的程序员提交assert()带有副作用的语句,这些语句会导致禁用断言的错误。例如

assert(function_that_should_always_be_called());

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

4

4 回答 4

18

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

想象一下,多年来积累了许多断言,但是由于没有使用 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

编译器帮助我找到了一个可疑的断言!另一方面,如果我替换++gg+1,链接错误就会消失,我不必调查。事实上,这种说法保证是无害的。

当然,可证明无副作用的概念受到优化器“可以看到”的限制。为了进行更精确的分析,我建议使用链接时优化 ( gcc -flto) 跨编译单元进行分析。

error作为一个轻微的可用性改进,在 GCC 4.4 和更高版本上,可以使用函数属性在编译时(而不是链接时)获得人类可读的错误消息。由于这个属性只对函数起作用,对变量不起作用,我们还需要告诉 GCC 它是一个纯函数,这意味着函数本身不会有副作用。这确保了如果返回值不相关,可以安全地删除对函数的调用。

extern int not_supposed_to_survive() __attribute__((pure)) __attribute__((error("assert() cannot be proven to have no side effects")));
#define assert(expr) do { (void)(not_supposed_to_survive() || (expr)); } while(0)

更新:我使用 gcc 5.3 在现实生活中的 C++ 代码库上应用了带有全局变量的简单变体。要使用链接时优化,您基本上将gcc -flto -g其用作编译器/链接器(编译器/链接器-g上的选项以获取链接错误的行引用)以及gcc-ar任何gcc-ranlib静态库的存档器/索引器。

这种设置可以极大地减少我必须调查的断言数量。用最少的人力,我就能把断言清理干净。我仍然不得不手动拒绝的误报是由于:

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

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

  • 包含日志语句的函数
  • 缓存其结果的函数
于 2016-02-09T14:18:16.580 回答
4

我不确定它对于您描述的应用程序是否足够,但 cppcheck 寻找“assertWithSideEffect”: http ://cppcheck.sourceforge.net/devinfo/doxyoutput/checkassert_8cpp_source.html

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

“Cppcheck 是 C/C++ 代码的静态分析工具。与 C/C++ 编译器和许多其他分析工具不同,它不会检测代码中的语法错误。Cppcheck 主要检测编译器通常不会检测到的错误类型。目标是只检测代码中的真正错误(即误报为零)。” http://cppcheck.sourceforge.net/

于 2017-02-03T16:51:05.443 回答
3

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

如果它不是一个足够简单的控制流,它怎么知道它是不是纯的?


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

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

将编译出几个表达式,包括带有__attribute__((pure)).

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

于 2012-05-15T02:47:58.220 回答
1

即使 GCC 可以可靠地检测到纯计算(这需要解决停机问题),标志也必须具有额外的魔力才能注意到非纯计算作为参数传递给您自己开发的断言宏。扩展也无济于事——它到底应该做什么?

您的问题的解决方案是

  1. 聘请有能力的开发人员。
  2. 教育您的开发人员如何使用断言(除其他外)。
  3. 进行代码审查。
  4. 针对可交付版本进行所有测试——如果断言在可交付成果中关闭,那么 assert(function_that_should_always_be_call()) 与简单地省略 function_that_should_always_be_call() 没有什么不同,这是一个应该在测试中发现的明显错误。
于 2012-05-15T05:13:47.690 回答