Seiteneffekte

Wenn wir irgendwo auf den Begriff Seiteneffekte stoßen werden die meisten von uns das mit etwas eher Negativen verbinden. Nebenwirkung klingt doch so ähnlich, oder? Und das ist ja auch nicht unbedingt etwas Positives. Schließlich setzten Pharmakonzerne doch einiges daran Nebenwirkungen zu beseitigen, denn ihre Medikamente sollten ja doch möglichst nur gegen eine bestimmte Krankheit wirken und nicht andere hervorrufen. Genau so sollten es wir Programmierer auch angehen. Wir sollten alles daran setzten, dass unser Code das tut was er soll und dabei keine unerwünschten Seiteneffekte hervorruft. Jetzt sind wir schon wieder auf den Begriff Seiteneffekt gestoßen und wissen immer noch nicht genau was er eigentlich bedeutet. Es ist eigentlich ganz einfach. Ein Seiteneffekt ist eine Effekt der unvorhergesehen bzw. nicht sehr leicht zu erkennen auftritt und somit leicht zu Fehlern führen kann.

Einfache Beispiele

Beispiele in C

Um das ganze etwas verständlicher zu machen schauen wir uns einmal ein einfaches Beispiel in C an. Da es vor allem bei Makros sehr leicht zu Seiteneffekten kommen kann nehmen wir das Parademakro schlechthin dafür, nämlich ein Makro das das Minimum zweier Werte zurückgibt. Schauen wir uns einmal den Code dazu an:

#define min(a,b) ( (a < b) ? (a) : (b) )

Dieses Makro tut nichts anders als a mit b zu vergleichen und dann, wenn der Vergleich 'wahr' ergibt a zurückgibt und sonst b. Wir können das Makro leicht testen und dabei feststellen, dass es wie erwartet funktioniert:

a = min(2,3);   // a == 2
b = min(19,-3); // b == -3
// ...

Soweit so gut. Aber statt zwei Zahlen können ja zwei Ausdrücke stehen die nur die Bedingung erfüllen müssen, dass man sie mit dem <-Operator vergleichen kann. Nehmen wir also einmal folgenden Aufruf:

a = b = 1;
c = min(++a,++b); // c == ?

Welchen Wert hat jetzt wohl c? Auf den ersten Blick werden wir wahrscheinlich auf 2 tippen, da anscheinend ++a mit ++b verglichen wird. Und da vor dem Vergleich beide Variablen den Wert '1' gehabt haben, sollten jetzt beide doch den Wert '2' haben. Und weil das Minimum von 2 und 2 nun einmal 2 ist erwarten wir das auch als Wert für c. Doch leider ist es nicht so einfach. Wir sind gerade auf einen der 'beliebtesten' Seiteneffekte gestoßen. Weil Makros nämlich wirklich wie Copy & Paste arbeiten schaut der betreffende Code der beim Compiler ankommt nämlich so aus:

c = ( (++a < ++b) ? (++a) : (++b) )

Jetzt können wir das Problem aber leicht erkennen. Zuerst werden a und b inkrementiert und verglichen. Beide haben jetzt also den Wert '2'. Da der Vergleich 'falsch' zurückgibt wird der Ausdruck nach dem : ausgewertet also ++b. Und hier sitzt der Quell allen Übels. b wird noch einmal inkrementiert weshalb b jetzt genauso wie 'c' den Wert '3' hat. Jetzt haben wir nicht nur einen ungewollten Seiteneffekt sondern auch ein falsches Ergebnis, da nach dem Vergleich b und c jeweils den '3' haben a aber nur '2' weshalb '3' natürlich nicht das Minimum ist.

Auch ein if kann gefährlich sein

Das Makro zur Bestimmung des Minimums zweier Werte ist natürlich nicht die einzige Quelle für Seiteneffekte. Selbst if-Abfragen können wieder in Kombination mit unseren geliebten Inkrement- oder Dekrementoperatoren schnell unerwünschte Seiteneffekte hervorrufen was folgendes Beispiel verdeutlichen sollte:

if( ++a < 5 ) /* Tu was sinnvolles */;
else if( ++a > 10 ) /* Tu was viel Besseres */;

Welchen Wert hat a wohl nach dieser Codepassage? Das hängt scheinbar vom Wert von a vor diesem Codeausschnitt ab. Wenn a kleiner als 4 ist dann ergibt der erste Vergleich bereits wahr und der zweite Vergleich wird nicht mehr durchgeführt. Durch den Inkrementoperator ist a jetzt also um eins erhöht worden. War a vorher jedoch größer bzw. gleich 4 dann wird die erste Bedingung nicht erfüllt und deshalb noch der zweite Vergleich durchgeführt wodurch a ein weiteres Mal erhöht wird und somit einen um zwei höheren Wert als vorher hat. Diese Verhalten ist eigentlich immer unerwünscht und nicht beabsichtigt, da es auf den ersten Blick nicht so leicht zum Durchschauen ist, was da eigentlich genau passiert.

Folgerung

Aus diesen Beispielen haben wir gesehen wie leicht Seiteneffekte auftreten können und das vor allem im Zusammenhang mit den Inkrement- und Dekrementoperatoren, weshalb wir diese vor allem bei Makros und if-Abfragen vermeiden sollten. Am Besten verwenden wir sie auch nicht bei Funktionsaufrufen, da nie garantiert ist, dass diese Funktion nicht auch einmal von einem Makro mit Seiteneffekten überdeckt werden wird.

Weitere problematische Fälle

Call by Reference

Neben den bereits erwähnten Quellen stellt auch die Übergabe von Parameter per Referenz ein Problem dar. Aus diesem Grund sollten solche Variablen innerhalb der betreffenden Funktion nur verändert werden wenn dies für den Aufrufer auch klar ersichtlich ist. Folgende Funktion ist ein Fall der in unseren Programmen nie vorkommen sollte:

void printMsg( char *msg )
{
  // Nachrichtenlänge einschränken:
  if( strlen(msg) > 15 )
    msg[15] = '\0';
  printf("Msg: %s\n", msg);
}

Auf den ersten Blick schaut diese Funktion doch recht harmlos aus. Sie erwartet eine Nachricht, überprüft ob die Länge 15 überschreitet und schreibt sonst an die 16. Stelle eine binäre Null, damit printf dort mit der Ausgabe aufhört. Doch jetzt schauen wir uns einmal die Verwendung der Funktion in folgendem Zusammenhang an:

char msg[] = "Das ist eine ganz tolle Nachricht mit mehr als 15 Zeichen ;)";
// ...
if( strlen(msg) > 20 )
{
  printMsg(msg);
  // ...
  printf("Nachricht hat mehr als 20 (%d) Zeichen: %s\n", strlen(msg), msg);
}

Wenn wir diesen Code jetzt ausführen dann kommt natürlich keine Nachricht mit mehr als 20 Zeichen heraus. In diesem Beispiel ist das natürlich auch sehr konstruiert und leicht zu „durchschauen“, aber wenn man nicht aufpasst kann sich so etwas leicht in einen Code hinein schleichen. Deshalb sollte eine Funktion immer nur genau das tun was man sich auf Grund ihrer Signatur bzw. Benennung erwarten würde. Unsere Funktion printMsg sollte also wirklich nur so wie ihr Name aussagt die Nachricht ausgeben aber nicht verändern. Und wir dürfen auf keinen Fall erwarten, dass jemand der unsere Funktionen verwendet diese immer anschaut oder die Dokumentation dazu liest. Es kann selbst reichen das wir selbst unseren eigenen Code einige Zeit lang nicht verwenden und uns dann wenn wir ihn wieder verwenden meistens nicht mehr an alle Seiteneffekte erinnern können. Deshalb sollten wir uns immer an den Grundsatz halten, dass eine Funktion immer nur das tun soll was ihr Name über sie aussagt.

Echte Referenzen

Noch gefährlicher für das Eintreten von Seiteneffekten sind Referenzen wie sie zum Beispiel in C++ oder PHP verwendet werden. Da der Aufrufer dabei ohne die Signatur der Funktion genauer zu betrachten nicht einmal erkennen kann das ein Wert per Referenz übergeben wird. Deshalb sollte man hier noch einmal vorsichtiger sein als bei der Übergabe mit Zeigern. Wir schauen uns das wieder an einem kurzen Beispiel an:

void printNumber( int& num )
{
  cout << "Number:" << num << endl;
  num *= 2; // Wir sind ganz böse und verdoppeln heimlich 'num' :-)
}
// ...
// Aufruf der Funktion:
int num = 2;
printNum(num);

Wer erwartet denn das nach diesem Aufruf num den Wert 4 hat? Natürlich niemand, denn dem Namen nach sollte die Funktion nur dir Zahl ausgeben aber nicht verändern. So extrem wird das wohl keiner machen, aber es sollte demonstrieren wie leicht ein versteckter Fehler entstehen kann, wenn man nicht aufpasst.

Abschluss

Diese Beispiele und Warnungen sollen natürlich niemanden davon abhalten Makros, Dekrement- bzw. Inkrementoperatoren oder Call by Reference zu verwenden, sondern sie sollen nur den Umgang mit ihnen sensibilisieren, da ein verantwortungsbewusster Umgang mit ihnen für guten Code in den meisten Sprachen notwendig ist.


Diskussion