我们有几个中等规模的C代码库,接收来自经验水平不同的开发人员的提交。一些纪律性较差的程序员会提交带有产生禁用断言时引起错误的副作用的 assert()
语句。例如:
assert(function_that_should_always_be_called());
我们已经使用自己的assert()
实现,但使用NDEBUG
定义评估表达式会导致无法接受的性能下降。是否有GCC扩展或标志可传递,以触发这些的编译时警告/错误?如果控制流程足够简单,那么GCC应该能够确定您只调用纯函数。我们有几个中等规模的C代码库,接收来自经验水平不同的开发人员的提交。一些纪律性较差的程序员会提交带有产生禁用断言时引起错误的副作用的 assert()
语句。例如:
assert(function_that_should_always_be_called());
我们已经使用自己的assert()
实现,但使用NDEBUG
定义评估表达式会导致无法接受的性能下降。是否有GCC扩展或标志可传递,以触发这些的编译时警告/错误?如果控制流程足够简单,那么GCC应该能够确定您只调用纯函数。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 4.4 及更高版本上,可以使用 error
函数属性在编译时(而非链接时)获得人类可读的错误消息。由于此属性仅适用于函数而不适用于变量,因此我们还需要告诉 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
作为存档程序/索引器。
这个设置可以极大地减少我需要调查的断言数量。在人力资源最小的情况下,我能够清理掉断言。我仍然需要手动关闭的虚函数调用、非平凡循环/递归(优化器无法证明它们是有限的)造成的误报仅占一小部分。
此外,我还会收到一些包含副作用但是无害或不重要的断言,例如:
assert()
内部有任何内存访问,例如void foo(int *p) { assert(p[0] > 0); }
,编译器就无法删除保护条件,因此这不起作用。 - yugrfoo
优化为一个空函数:https://godbolt.org/z/e69xwZ - Bruno De Fraine我不确定这是否足够适用于你所描述的应用程序,但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/
如果控制流程足够简单,GCC应该能够确定您只调用纯函数。
如果控制流程不够简单,它将如何知道它是纯的还是不纯的?
类似以下代码可能是最佳选择:
#ifdef NDEBUG
#define assert(s) do { (s); } while(false)
#else
// ...
#endif
一些表达式将被编译掉,包括使用 __attribute__((pure))
的函数。
最合理的解决方案是审查您的代码并修复错误。
assert()
的正确使用,其中表达式没有副作用 - 只要启用了优化,编译器就能够省略代码。在这里进行 (void)
强制转换也很有用,因为它可以防止编译器警告没有副作用的语句。 - cafassert
是非常糟糕的实践。问题是如何使assert
捕获错误,而不是让它工作。此外,如果执行类似于assert(check_data_structures())
的操作,这可能会严重降低性能。 - ugorengrep
查找警告没有效果的语句。然后,你可以在代码中查找所有的 assert
。任何不匹配都可能是潜在的副作用。这个过程可以自动化。挑战在于如何以安全的方式“永久”处理模糊的情况。 - Aaron McDaid即使GCC能够可靠地检测纯计算(这需要解决停机问题),一个标志也必须具有额外的神奇力量,以注意到非纯计算作为参数传递给您自己编写的assert宏。扩展也无法帮助--它应该做什么呢?
解决您问题的方法是: