Assertions in C
An assertion, in general, is a statement that affirms that a certain condition is true at a specific point in the program. It is not a normal flow control statement, but rather a means to verify that the program respects certain assumptions during execution.
It is a powerful tool for code verification, as it allows developers to quickly identify logical errors or unexpected conditions during program execution. Assertions are often used during the development and debugging phase to ensure that the code works as expected.
In C language, assertions are implemented through the assert macro, defined in the <assert.h> header. In this lesson we see how they work.
- Assertions are statements that verify that a certain condition is true during program execution.
- In C language, assertions are implemented through the
assertmacro, defined in the<assert.h>header. - If an assertion fails, the program prints an error message and terminates execution.
- Assertions are useful for debugging and code verification, but can be disabled in production by defining the
NDEBUGmacro.
Assertion Definition
When creating programs or software in general, algorithms are developed that are based on certain assumptions. For example, in a loop that iterates over an array, one can assume that the index never exceeds the length of the array itself.
Note that such assumptions do not concern the verification of input data. For example, it is normal to check whether the divisor of a division is different from zero before performing the operation. Assertions, instead, are used to prove that the code is correct.
Let's see a practical example. Suppose we have a function that sorts an array of integers using the bubble sort algorithm. One can assume that, after the function executes, the array is actually sorted. If this were not the case, it would not be an input error, but a logical error in the algorithm implementation.
#include <stdio.h>
bool is_sorted(int arr[], int size) {
for (int i = 1; i < size; i++) {
if (arr[i - 1] > arr[i]) {
return false;
}
}
return true;
}
void bubble_sort(int arr[], int size) {
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int data[] = {5, 2, 9, 1, 5, 6};
int size = sizeof(data) / sizeof(data[0]);
bubble_sort(data, size);
if (is_sorted(data, size)) {
printf("The array is sorted correctly.\n");
} else {
printf("Error: the array is not sorted!\n");
}
return 0;
}
The is_sorted function verifies whether the array is sorted. If, after calling bubble_sort, the array is not sorted, it means there is an error in the algorithm implementation.
The C language provides a simple way to express these assertions through the assert macro.
Using the assert Macro
In the <assert.h> header file, the assert macro is defined, which allows verifying a specific condition during program execution. The syntax is as follows:
#include <assert.h>
void assert(scalar expression);
Although assert looks like a function, it is actually a macro.
Each time it is invoked, the macro evaluates the expression passed as an argument. If the expression is true, that is, it has a value different from zero, the program continues execution normally as if nothing had happened.
If the expression is false (that is, it has a value of zero), the assert macro prints an error message to standard error (stderr) and brutally terminates the program by calling the abort() function.
Let's take the previous example and use the assert macro to verify that the array is sorted after the sorting function executes:
#include <stdio.h>
#include <assert.h>
bool is_sorted(int arr[], int size) {
for (int i = 1; i < size; i++) {
if (arr[i - 1] > arr[i]) {
return false;
}
}
return true;
}
void bubble_sort(int arr[], int size) {
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int data[] = {5, 2, 9, 1, 5, 6};
int size = sizeof(data) / sizeof(data[0]);
bubble_sort(data, size);
assert(is_sorted(data, size)); // Verify that the array is sorted
printf("The array is sorted correctly.\n");
return 0;
}
Now, let's add (intentionally) an error in the sorting algorithm to see how the assertion works:
void bubble_sort(int arr[], int size) {
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - i - 1; j++) {
// Intentional error: incorrect comparison
if (arr[j] < arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
Now, trying to compile and run the program with the error, what happens is that the assertion fails, and the program prints an error message similar to this:
Assertion failed: is_sorted(data, size), file example.c, line 20
Aborted (core dumped)
Some important notes
There are slight differences in the structure and behavior of the assert macro depending on the version of the C standard used (C89, C99, C11, etc.), but the basic concept remains the same.
In particular, in the C89 standard it was required that the argument of assert have the type int, therefore be an integer. The C99 standard relaxes this requirement and only requires that the argument be a scalar expression, that is, an expression that produces a single value (such as int, float, char, etc.).
Furthermore, the C99 standard requires that, in case of assertion failure, an error message be printed that includes the file name and line number where the failure occurred, thus improving debugging capability. Obviously, the precise form of the message may vary depending on the compiler implementation.
If we use gcc, typically the form of the error message will be similar to that shown in the previous example:
Assertion failed: <expression>, file <file_name>, line <line_number>
Disabling assertions
Assertions are a powerful debugging tool that allows us to verify code correctness during development.
However, they have two main disadvantages:
- They slow down program execution, since each assertion requires a condition evaluation.
- They can abruptly interrupt program execution in case of assertion failure. These interruptions are not handled and could cause data loss or inconsistent states.
For these reasons, when releasing a program in production, that is, when distributing the software to end users, it is not good practice to keep assertions active.
To disable assertions in a C program, it is not necessary to modify the source code. Instead, it is sufficient to define the NDEBUG macro (which stands for "No Debug") before including the <assert.h> header.
The definition of NDEBUG can be done in two ways:
-
By adding a
#definedirective at the beginning of the source file, before the inclusion of<assert.h>:#define NDEBUG #include <assert.h>In this way, all assertions in the source file will be disabled.
-
By passing the
-DNDEBUGoption to the compiler during program compilation. For example, if usinggcc, the compilation command could be:gcc -DNDEBUG -o my_program my_program.c
In both cases, when NDEBUG is defined, all calls to the assert macro are ignored by the compiler. Consequently, the program will not perform any assertion verification during execution, improving performance and avoiding unwanted interruptions.
Never insert code with side effects inside an assertion
One should always avoid using, inside an assertion, code that produces side effects.
In fact, let's take the following example:
assert((p = malloc(n)) != NULL);
In this case, if NDEBUG is defined, the assertion is ignored and the call to malloc is not executed. Consequently, the variable p is not initialized, which can lead to unexpected behaviors in the program.