Revisiting C/C++ basics: assert

Posted on . Updated on .

I work with C/C++ code five days a week but I don’t post much on the topic, so I decided to write a piece about assert() today. This is in part motivated because I made a mistake this week, got sloppy while programming something close to a deadline and forgot one of assert()'s features I normally don’t worry about. Not an issue as testing caught the bug but it made me relearn some basic concepts and I wanted to share them.

The assert basics

As you know, assert() is a standard macro declared in stdlib.h and cstdlib. It’s normally used to make sure certain conditions are met at specific points in the code. Failing to meet that condition is considered a bug in the program logic, and assert() prints a (developer) helpful error message before aborting the program.

Standard assert() macro has an often forgotten feature. If the NDEBUG macro is defined prior to including assert.h or cassert, assert() is turned into a no-op and its expression is not evaluated.

This behavior can cause bugs that only appear in release builds when the expression to evaluate has side effects. This is all mentioned clearly in most manpages documenting the macro. Some frameworks define NDEBUG for release builds. Notably, CMake comes to my mind, which defines NDEBUG for its Release, RelWithDebInfo and MinSizeRel build types.

The framework we use to build the software normally does not define NDEBUG under any circumstances. However, what I was writing this week was built using a different environment that defined NDEBUG and it caught me by surprise.

In addition, sometimes we want to assert an expression even in release builds. More so when we don’t want or can’t prove that testing the assertion slows the program down significantly. This is highly unlikely in most cases and I consider leaving the asserts in release builds a good practice then. Furthermore and specially if asserts are to be evaluated in release builds, if the assertion fails we probably want the program to crash in a controlled manner, maybe collecting printing additional information so the user can report the problem, both for GUI and CLI applications.

Solutions

To avoid release-build-only bugs, we should make sure the asserted expression does not have any side effects. For example:

class Foo
{
public:
	// ...
	size_t size() const;
	bool test_and_set(int value);
// ...
};

// Later in the code...

Foo foo;
assert(foo.size() > 0);      // Good. size() is const and has no side effects.
assert(foo.test_and_set(1)); // BAD! test_and_set probably has side effects.

The test_and_set method will not be called if NDEBUG is defined in a release build prior to including the assert.h or cassert standard headers.

Apart from that, we may want to assert expressions even in release builds with NDEBUG and provide a way to crash in a controlled manner. This can be achieved by using a set of custom assertion macros, with separate cases for assertions that should be removed from release builds and assertions that should be present in release builds too.

The following code contains an attempt at creating such a set of macros. It is partly copied without shame from an excellent post on the topic at Stack Overflow.

The standard assert() macro is required to be a non-void expression with a value that could be used in a boolean test, for example. The following macros aim for this behavior too, which discards using common macro compositions like “do …​ while(0)”. ASSERT is a macro that will always be evaluated, while DASSERT mimics standard assert() and is a no-op when NDEBUG has been defined, hence only being evaluated in debug builds and not release builds.

Users can provide their own assertion handlers to perform additional actions on assertion failure as well as controlling whether the program crashes or not via abort().

Hopefully the header comments should be clear enough.

xassert.h
#ifndef XASSERT_HEADER_
#define XASSERT_HEADER_
/*
 * THIS HEADER REQUIRES stdlib.h or cstdlib TO BE INCLUDED PREVIOUSLY.
 *
 * This header defines two macros and a function.
 *
 * ASSERT is a macro that will always check the expression passed to it. If the
 * expression evaluates to false, it will run the assertion handler. If the
 * assertion handler returns true, it will halt the program by calling
 * abort(3).
 *
 * DASSERT is a macro that does the same except that it will be a no-op if
 * NDEBUG is defined, like standard assert(3). The assert expression will only
 * be evaluated in debug environments.
 *
 * set_assert_handler() is a function that allows users to pick their own
 * assertion handler different from the default provided one. Assertion
 * handlers should conform to the following prototype:
 *
 *     int (*function)(const char *expression, const char *file, int line);
 *
 * The default assertion handler will print the expression, line and file to
 * standard error and return true so the program is aborted. The value returned
 * from set_assert_handler() is the old assertion handler function in case it
 * needs to be restored. set_assert_handler() is NOT reentrant.
 */

typedef int (*assert_handler_ptr_t)(const char *, const char *, int);

#ifdef __cplusplus
extern "C" {
#endif
assert_handler_ptr_t set_assert_handler(assert_handler_ptr_t);
extern assert_handler_ptr_t assert_handler_ptr;
#ifdef __cplusplus
}
#endif

#ifdef __cplusplus
#define HALT() (std::abort())
#else
#define HALT() (abort())
#endif

#define ASSERT_HANDLER(x, y, z) ((*assert_handler_ptr)(x, y, z))

#define ASSERT(x) (!(x) && ASSERT_HANDLER(#x, __FILE__, __LINE__) && (HALT(), 1))

#ifndef NDEBUG
#define DASSERT(x) ASSERT(x)
#else
#define DASSERT(x) (1)
#endif

#endif
xassert.c
#include <stdio.h>
#include "xassert.h"

#ifdef __cplusplus
extern "C" {
#endif

static int
assert_handler_default(const char *expr, const char *file, int line)
{
    fprintf(stderr,
            "Assertion failed on %s line %d: %s\n"
            "Please report this problem. Aborting program.\n",
            file, line, expr);
    return 1;
}

assert_handler_ptr_t
set_assert_handler(assert_handler_ptr_t handler)
{
    assert_handler_ptr_t old = assert_handler_ptr;
    assert_handler_ptr = handler;
    return old;
}

assert_handler_ptr_t assert_handler_ptr = assert_handler_default;

#ifdef __cplusplus
}
#endif

Comments about possible improvements or problems with the above code are welcome.

Further thoughts

Messages printed by assertion macros are usually not user-friendly. However, I like to use assert() sometimes to check for conditions that should not be present when running the program normally and cannot be attributed to simple user configuration or runtime errors.

For example, suppose a given program uses an image file as a resource and looks for it in a standard location known at installation time. Under normal circumstances, the image should not be missing or corrupted at all. If the image is missing, it’s because either something went wrong with the installation process or someone deleted it by accident, or there’s a disk problem, etc. When opening the image, you probably want to assert() it could be opened without any problems, but a failure to assert that condition is not a program bug.

If that assertion fails, the user may not be at fault and the error message may not be helpful at all. For these cases, a third assert macro variant could be used, not included above. One which takes an string argument in addition to the asserted expression and uses it to notify the user of the unusual error condition in a more friendly way.

Update: I created a repository in Github to hold this code with more corrections and code from the “Further thoughts” section.

Load comments