https://new.jameshunt.us

Assertions and Intentions— or —Run-time Safety, All The Time

I love the concept of assertions, little bits of code that exist to make sure that all the other bits of code are playing by the rules. Contrast that with intentions, which aren't in code at all, often existing only in the documentation or, worse, in the original programmer's head.

The Meat, The Potatoes

Consider this function to count the length of a '0'-terminated string:

int _strlen(const char *s)
{
    int n = 0;
    while (*s++)
        n++;
    return n;
}

There's an intention here; the caller really shouldn't pass NULL for the s parameter. The first time the code tests the conditional in the while loop, the program is going to segfault.

Let's turn that into an assertion:

#include <assert.h>

int _strlen(const char *s)
{
    assert(s != NULL);

    int n = 0;
    while (*s++)
        n++;
    return n;
}

Now we have an assertion, and the program will check itself to make sure that some other part of the program didn't accidentally try to calculate the length of NULL.

Of course, the only respectable course of action to pursue when an assertion is violated is to abort the program, but at least you get an error message to the effect of "assertion failed".

In fact, let's try it out by deliberately sabotaging ourselves:

#include <assert.h>
#include <stdio.h>

int _strlen(const char *s)
{
  assert(s != NULL);

  int n = 0;
  while (*s++)
    n++;

  return n;
}

int main(int argc, char **argv)
{
  _strlen(NULL); /* should fail */
  return 0;
}

And when we run it?

→  ./traditional
Assertion failed: (s != NULL), function _strlen, file traditional.c, line 6.
Abort trap: 6

That's exactly what we want to see. If we run a sufficient battery of tests against our function, we should see the assertion bomb out, and our tests fail.

But assert() has some problems of its own, which we should talk about.

Problems With assert()

I see two main issues: reliability and messaging.

1. It can be disabled at build time.

You can't rely on your assert()-based assertions to actually fire. Whoever is building your software could just set CPPFLAGS=-DNDEBUG and disable them altogether.

From the assert(3) man page:

The assert() macro may be removed at compile time with the
cc(1) option -DNDEBUG.

2. Messages printed on assertion failure are pretty basic.

Sure, you get the function, source file and line number in the error message, along with the test itself (s != NULL), but you can't add an explanation for the human operators who will invariably see this message in the logs some day.

Even having the function / file / line number is of dubious value, because code changes over time. Functions get renamed, files split or merged. The assertion on line 432 might be on line 467 in v1.0.1, replaced in v1.1.9, and removed outright in v2.0.0. All of these factors conspire to make it difficult to trace down problems even in F/OSS software where you have unfettered access to the source code!

The Solution! insist()

I wrote a replacement assertion macro that I call insist():

#include "insist.h"

int _strlen(const char *s)
{
    insist(s != NULL, "_strlen(NULL) is undefined");

    int n = 0;
    while (*s++)
        n++;

    return n;
}

Disabling it is harder (it's still possible, but it won't be done on accident by a well-meaning package maintainer), and you get to specify a mesage that prints when the assertion fails.

Hopefully you find it useful. You can get the code here. It's licensed MIT, so you can embed it in your project with almost no restrictions. It's also really small (<100 lines, including the copyright notice!)

Happy Hacking!

James (@iamjameshunt) works on the Internet, spends his weekends developing new and interesting bits of software and his nights trying to make sense of research papers.

Currently exploring Kubernetes, as both a floor wax and a dessert topping.