Sequenz-Punkte

Wenn innerhalb eines Ausdruckes ein Operand mehrfach auftritt, so können bei einer Änderung des Operanden Nebeneffekte auftreten. Dies kann in seltenen Fällen zu Unbestimmtheiten führen, welche das Ergebnis eines Ausdruckes unvorhersehbar machen. Je nachdem, wie der Ausdruck geschrieben, mit welchem Compiler er übersetzt und in welcher Umgebung er ausgeführt wird, können andere Resultate entstehen. Durch die Festlegung von sogenannten Sequenz-Punkten wird in den Sprachen C und C++ festgelegt, wann sämtliche Nebeneffekte abgearbeitet sein müssen.

Details

Zwar ist in den Sprachen C und C++ durch die Abarbeitungsrichtung und Rangfolge von Operatoren genau spezifiziert, wann welcher Operator eines Ausdruckes abgearbeitet werden soll. Es ist jedoch nicht festgelegt, wann die Operanden des Ausdruckes ausgewertet werden sollen. Wenn in einem Ausdruck ein Operand mehrfach auftritt und sich dieser Operand irgendwann während der Abarbeitung des Ausdruckes verändert, kann diese Änderung als Nebeneffekt die verbleibenden Auswertungen des Operanden beeinflussen. Je nachdem, ob der vom Compiler erstellte Maschinencode den Operanden vor oder nach der Änderung anspricht (lesen oder schreiben), kann es sein, dass unterschiedliche Resultate hervorgehen.

In C und C++ wurden aus diesem Grund für einige wichtige Fälle sogenannte Sequenzpunkte definiert. Bei einem Sequenzpunkt kann der Programmierer davon ausgehen, dass sämtliche vorangegangene Nebeneffekte abgearbeitet und somit abgeschlossen sind. Zwischen Sequenzpunkten können jedoch nach wie vor Nebeneffekte zu Problemen führen.

Folgendes sind die Sequenzpunkte in C und C++:

Diese Punkte decken die wichtigsten Sequenzpunkte ab. Für Details über den präzisen Zeitpunkt der Sequentialisierung sowie zusätzliche Angaben über die Unterschiede in C und C++ wird auf andere Quellen verwiesen.

Ungelöste Probleme

Das Auftreten von unerwünschten Nebeneffekten wird durch Sequenzpunkte stark vermindert, jedoch nicht gänzlich vermieden. Beispielsweise ist die Reihenfolge der Auswertung bei den allermeisten Operatoren (ausser die oben genannten) NICHT definiert. Ebenfalls NICHT definiert ist die Reihenfolge der Auswertung der Argumente bei einem Funktionsaufruf. Es ist hierbei zu beachten, dass bei der Trennung der Argumente mittels Komma , es sich NICHT um den Sequenz-Operator handelt. Für Beispiele dieser Nebeneffekte wird auf andere Quellen verwiesen.

Die bekanntesten Vertreter von Nebeneffekt-Quellen sind die Inkrement- und Dekrement-Operatoren. Beispielsweise ist es im folgenden Code nicht möglich, den schlussendlichen Wert von x vorauszusagen:






10? 11?
#include <stdio.h>

int main(){
  int x = 10;
  x = x++;
  printf("%d\n", x);
  return 0;
}

In diesem Beispiel ist es nicht klar, ob die Zuweisung vor oder nach Ausführung des Inkrements geschehen soll. Entweder wird zuerst das Inkrement ausgeführt und danach der ursprüngliche Wert (was dem Rückgabewert der Operation x++ entspricht) wieder zugewiesen, oder aber der ursprüngliche Wert wird zuerst zugewiesen und danach die Variable inkrementiert.

Abschliessend sei bemerkt, dass Nebeneffekte in der alltäglichen Programmierung äusserst selten eine Rolle spielen. Sie gehören zu den obskuren Spezialfällen, welche normalerweise nur bei äusserst extensivem Gebrauch von komplexen Ausdrücken zu Problemen führen können. Durch die Sequenzpunkte werden bereits viele Problemfälle vermieden. Auf weitaus mehr Probleme können im Programmieralltag bei falschen oder vergessenen Klammerungen sowie unbeabsichtigten arithmetischen Umwandlungen auftreten.

Verwandte Probleme

Nebeneffekte sind auch bekannt beim Ansprechen von Arrays unbekannter Grösse. Oftmals werden Arrays einer Funktion mittels Pointer als Argumente übergeben, wodurch die tatsächliche Grösse der Arrays dem Compiler innerhalb der Funktion nicht bekannt ist. Mittels dem restrict-Keyword kann dem Compiler mitgeteilt werden, dass sich zwei adressierte Bereiche im Speicher nicht überlappen und sie somit bei gleichzeitigem Zugriff keine Nebeneffekt-Quelle darstellen können. Der Vorteil hiervon ist jedoch weniger eine definierte Sequentialisierung denn mehr eine verbesserte Optimierung des Maschinencodes.

Sequenzpunkte dienen dazu, Lese- und Schreibzugriffe zu sequentialisieren, also für die obengenannten Situationen eine garantierte Auswertungsreihenfolge festzulegen. Solange ein Programm als ein komplett unabhängiger Prozess betrachtet werden kann, ist die Sequentialisierung schlussendlich durch das Compilat festgelegt und sämtliche Zugriffe auf einen Wert sind dadurch synchronisiert. Ist das Programm jedoch nicht unabhängig, so kann es sein, dass sich Werte von Operanden verändern, ohne dass dies der Programmierer voraussehen kann. Ein solcher Zugriff wird als asynchronen bezeichnet.

Asynchrone Zugriffe können auftreten, wenn beispielsweise das Betriebssystem einen Interrupt ausführt, welcher einen bestimmten Wert ändert, beispielsweise, wenn ein Peripheriegerät neue Daten an das Programm sendet. Für diesen Fall gibt es in den Sprachen C und C++ das volatile-Keyword, welches bewirkt, dass der Wert einer Variablen niemals zwischengespeichert, sondern stets neu ausgewertet wird. Hierbei kann es natürlich vorkommen, dass sich der Wert einer Variablen während der Ausführung eines kompizierten Ausdrucks verändert und somit das Resultat nicht vorhersehbar ist. Heutzutage werden für solche System- oder Peripherie-Interrupts Funktionen vom Betriebssystem angeboten, welche die asynchronen Zugriffe steuern und so einen geregelten Ablauf der Zugriffe sicherstellen.

Bei der Ausführung von mehreren parallelen Prozessen (oder Threads) kann es sein, dass ein Wert von einem Prozess geändert wird, währenddessen sich ein anderer Prozess soeben in der Abarbeitung eines Ausdruckes befindet, welcher auf diesen Wert mehrfach (lesend oder schreibend) zugreift. Auch diese Zugriffe passieren asynchron und das Resultat ist nicht vorhersehbar.