Über C und C++ Assertions

Über C und C++ Assertions

Es herrscht bei vielen Softwareentwicklen eine ziemliche Verwirrung über Sinn und Nutzen von Assertions.

Der folgende Artikel gibt eine kleine Einführung in die verschiedenen Assertions von C und C++. Dabei muss zwischen statischen und dynamischen Assertions unterschieden werden.

Was ist von folgender Aussage zu halten? Assert all function arguments and return values, pre/postconditions and invariants. A function must not operate blindly on data it has not checked. The purpose of a function is to increase the probability that a program is correct. Assertions within a function are part of how functions serve this purpose. The assertion density of the code must average a minimum of two assertions per function.“ *

Wie wir sehen werden, ist diese Aussage ein wenig mit Vorsicht zu genießen.

Static Assertions

Static Assertions sind nie ein Problem. Sie werden zur Compilezeit ausgeführt, sind damit aber auch nur für Bedingungen geeignet, die zur Compilezeit ausgewertet werden können.

Falls die Assertion fehlschlägt, gibt es einen Compilerfehler. Das Programm kann also nie zur Ausführung kommen.

static_assert(sizeof(void*) == 4);

Mit dieser Anweisung kann beispielsweise sichergestellt werden, dass Code, der nur für 32 Bit geeignet ist, nicht auf anderen Plattformen benutzt wird.

Dynamic Assertions

Dynamische Assertions werden zur Laufzeit geprüft. Trifft die geprüfte Bedingung nicht zu, wird üblicherweise das Programm mit Ausgabe der Codestelle beendet.

Allerdings haben dynamische Assertions Vor- und Nachteile.

int f(int* result)
{
    assert(result != nullptr);
    *result = 3:
}

Die Assertion ist zunächst mal gut, da geprüft wird, ob der Zeiger gültig ist. Allerdings gibt es den Check üblicherweise nur im Debug-Build. Damit ist die Assertion gut, um Fehler zu finden, da sie im Debug-Build auffallen. Man muss aber aufpassen, keine Überprüfungen auszulassen, die jederzeit nötig sind.

Für Parameter von außen – Benutzereingaben, Daten aus Fremdcode – ist die Assertion also nicht ausreichend!

In C gibt es sprachbedint viele Pointer. Diese werden häufig wie C++-Referenzen benutzt, das heißt, der Pointer kann und darf nicht NULL sein. Bei internen – insbesondere statischen – Funktionen hat sich folgendes Verfahren bewährt: Annotation an den Pointer, dass er nicht NULL sein darf und eine Assertion um fehlerhafte Aufrufe zu finden.

// Der Pointer darf nicht NULL sein. 
// Die gerufene Funktion checkt den Pointer eventuell nicht.
// Der Aufrufer muss den Pointer prüfen.
#define NEVER_NULL_PTR

// Aufgerufene Funktion
void f(NEVER_NULL_PTR int* result)
{
   assert(result);
   *result = 2;
}

// Aufrufer
void g(int* result)
{
   if (result != NULL)
   {
      f(result);
   }
}

Fazit

Dynamische Assertions sind eher wie ein eingebauter Unit-Test. Wichtige Run-Time-Checks dürfen auf keinen Fall ausgelassen werden!

* Zitat aus https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md