Catching assert() with side effects
assert, c, gcc
Solution
Despite the many unhelpful non-answers this question has received, I think it has a lot of merit in a context of a legacy code base.
Imagine that many assertions have been accumulated over the years, but because there wasn't a habit of building/testing with NDEBUG, some side-effects have trickled into the assertions and now you don't dare to disable the assertions anymore.
You may turn on NDEBUG and detect some test failures in your test suite, but it is totally not straightforward to link a test failure to the 'effectful' assertion because it may be very far from the point where you detect the failure. And even a test suite with good coverage cannot be trusted to be complete.
You may conduct a code review of all assertions in the code, but this is potentially a lot of work and prone to human error. It would be much better if some static analysis can already eliminate all assertions where it can prove that no side-effects appear and you only have to investigate those cases where their absence is not guaranteed.
Here is how you can use the optimizer of your compiler to conduct such a static analysis. Suppose that you organize to replace the definition of the `assert` macro by:
extern int not_supposed_to_survive;
#define assert(expr) ((void)(not_supposed_to_survive || (expr)))
If `expr` has any side-effect, the execution of the effect is conditional on the value of global variable `not_supposed_to_survive`. But if `expr` does not to have any side-effect, the value of the global variable does not matter (note that the `expr` result is discarded). A good optimizer knows this and will eliminate the load of global variable `not_supposed_to_survive`, hence the name of the variable.
If our program does not contain a definition of the symbol `not_supposed_to_survive`, we will get a link error when the load is not eliminated and we can use this to detect a potentially effectful assertion.
E.g. with 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
The compiler helped me find a dubious assertion! On the other hand, if I replace `++g` by `g+1`, the link error disappears and I don't have to investigate. Indeed, that assertion is guaranteed harmless.
Of course, the concept of provably side-effect free is limited by what the optimizer "can see". For a more precise analysis, I would recommend using link-time optimization (`gcc -flto`) to analyze across compilation units.
As a slight usability improvement, it's possible on GCC 4.4 and higher to obtain a human-readable error message on compile time (instead of link time) using the `error` function attribute. Since this attribute works only on functions and not on variables, we additionally need to tell GCC that it's a pure function, which means that the function itself will have no side effects. This ensures that calls to the function can safely be dropped if the return value is not relevant.
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)
Update: I applied the simple variant with the global variable on a real life C++ code base using gcc 5.3. To use link-time optimization, you essentially use `gcc -flto -g` as the compiler/linker (the `-g` option on compiler/linker to get a line reference on link errors) and `gcc-ar` and `gcc-ranlib` as the archiver/indexer for any static libraries.
This setup could tremendously reduce the number of assertions I had to investigate. With minimal manpower I was able to get the assertions clean. The false positives that I still had to turn down manually were due to:
- Virtual function calls
- Non-trivial loops/recursions (where the optimizer can not prove they are finite)
Additionally, I would also get some assertions that indeed contained side-effects, but they are harmless or not significant, such as:
- Functions containing logging statements
- Functions that cache their result(s)
Problem
We have several moderately sized C code bases that receive commits from developers with a variety of experience levels. Some of the less disciplined programmers commit `assert()` statements with side effects that cause bugs with assertions disabled. E.g. ``` assert(function_that_should_always_be_called()); ``` We already use our own `assert()` implementation, but evaluating the expression with `NDEBUG` defined would cause unacceptable performance degradations. Is there a GCC extension or flag we can pass that will trigger compile time warnings/errors for these? With simple enough control flow it should be possible for GCC to determine that you are only calling pure functions.