Better looking assertions in C

Oct. 12, 2018, 9:05 p.m.

Assertions are very important for developers, because they help developers to find bugs. This concept should be used to check that the internal state of our application is correct. Some developers even measure a quality of the project/code based on the amount of assertions. In many programs assertions are enabled only in developer modes and not in production. Programmers should always remember that assertion can’t be used to validate user input because it can introduce some bugs into the code. Although this topic is very interesting this is not what we will talk about today. Today let’s discuss how to make our assertions be even more useful.

In C, assertions are implemented using macros. Macros allow us to report not only which of our predictions were incorrect, but with them we can also report the name of the file, function or the line in the file where the assertion was incorrect.

#define ASSERT(exp) do {                            \
       if (!(exp)) {                                \
              fprintf(stderr, "ASSERT %s:%d %s\n",  \
                  __FILE__, __LINE__, #exp);        \
              abort();                              \
       }                                            \
} while(0)

inline void
assert(bool exp)
{
       if (!exp) {                               
            fprintf(stderr, "ASSERT %s:%d\n", __FILE__, __LINE__);
            abort();                    
       }                                       
}

int
main()
{
       int a, b;

       a = 1;
       b = 2;
       ASSERT(a == b);
       assert(a == b);
}

To stop compilation just after the preprocessor phase we can use the `-E` options.

$ gcc -E file.c
inline void
assert(bool exp)
{
       if (!exp) {
              fprintf(stderr, "ASSERT %s:%d\n", "x.c", 13);
              abort();
       }
}

int
main()
{
       int a, b;
       a = 1;
       b = 2;
       do { if (!(a == b)) { fprintf(stderr, "ASSERT %s:%d %s\n", "x.c", 22, "a == b"); abort(); } } while(0);
       assert(a == b);
}

The preprocessor removed our macro in the place when it was used. The __FILE__ and __LINE__ macros were replaced respectively to the filename and the line number. The variable which was preceded with the ‘#’ (#exp) was replaced by the statement used as an argument in the macro (“a == b”). In the case of the inline function we are unable to obtain which statement was incorrect, and the __FILE__ and __LINE__ macros don't return us the information about usage of inline function. The __FILE__ and __LINE__ macros are replaced by the filename and line where inline function is defined and not used.

In many cases the information printed by the macro assertion is enough for us for debugging. We assumed that the ‘a’ and ‘b’ is equal but our statement was incorrect, but what was the value of them? In Solaris we can find a nice set of macros which helps us with that.

#define ASSERT3U(LEFT, OP, RIGHT) do {                         \
       const uint64_t __left = (LEFT);                         \
       const uint64_t __right = (RIGHT);                       \
       if (!(__left OP __right)) {                             \
               fprintf(stderr, "ASSERT %s:%d %ju %s %ju\n",    \
                   __FILE__, __LINE__, __left, #OP, __right);  \
               abort();                                        \
       }                                                       \
} while(0)

In Solaris we will find a few ASSERT functions. For example, the ASSERT3U for unsigned numbers, the ASSERT3S for signed numbers, and the ASSERT3P for the pointers. This distinguishing must be done because different format strings in the fprintf(3) must be used and different variables need to be defined. Above we have a simplified version of ASSERT3U. At the beginning of the macro we assign the values passed to it to a new variable. We use the LEFT and RIGHT variables twice, so if we don’t assign them to a variable the usage of this assertion like ASSERT3U(a++, ==, b) would increment the left variable twice and we would got an incorrect debugging message. Please also notice that using a expressions with side effects with assertion are a bad pattern and should be avoided. The value of your program would be changed depending on DEBUGs flags.

The ASSERT3*() macros have some downsides as well. The syntax have changed – the values and the operand need to be separated by a comma (usage ASSERT3U(a, ==, b)). We also can’t use those macros in case of more complicated assertions with multiple conditions. However, in many cases assertions are simple, so you can find the additional information printed by the ASSERT3*() very useful.

UPDATE #1: silverk_ from reddit pointed out that ASSERT should be printed on stderr. 
UPDATE #2: spc476 from lobsters pointed out that the a++ syntax should be documented as a bad pattern.