Die letzten drei Tage hab ich einen Fehler aus der Kategorie "In den Fuß schießen mit C++" gesucht. Da gibt man sich nun schon alle Mühe, verwendet die gut getesteten Datenstrukturen der STL und trotzdem grinst einen ein ums andere Mal ein Segmentation Fault an. *gnaa*
Folgender Code (vereinfacht, das Original ist in eine Klasse eingebettet):
#include <iostream>
#include <list>
std::list<int> bla;
std::list<int> const getBla() {
return bla;
}
int main() {
bla.push_back(10);
for (std::list<int>::const_iterator i=getBla().begin(); i!=getBla().end(); ++i) {
std::cerr << *i << std::endl;
}
}
getBla() ist im Original ein Getter für ein Klassenmember. Eigentlich ein gängiges Konstrukt. Der Crash passiert in der Schleife bei der Dereferenzierung des Iterators i. (Im Original war das etwas komplizierter, aber das ist hier nebensächlich.). Nach einigem Rumprobieren stelle ich fest, daß die Schleife zweimal durchlaufen wird, obwohl bla ja eigentlich nur ein Element enthält. Die Dereferenzierung des Iterators knallt im zweiten Durchlauf der Schleife (logisch, der zeigt dann ja irgendwo ins Nirvana).
Die Ursache des Fehlers liegt in einem fehlenden Zeichen in der Deklaration von getBla(). Eigentlich müßte das wie folgt aussehen (der Unterschied ist der Einfachheit halber rot markiert):
[...]
std::list<int> const &getBla() {
[...]
Standardmäßig übergibt C++ Parameter und Rückgabewerte by value, d.h. es wird eine Kopie des Wertes auf dem Stack gemacht. Das ganze ist von C geerbt. Mit der Einführung von abstrakten Datentypen kann so eine Kopie natürlich richtig teuer werden, weswegen zusätzlich die Möglichkeit der Übergabe by reference eingeführt wurde. Gekennzeichnet wird solch eine Referenz durch ein & hinter dem Datentyp. Dann wird eben statt der Kopie nur ein Verweis übergeben. Kann ungeheuer Zeit und Speicherplatz sparen.
Wie führt das nun in dem Code zum Crash? Nun dafür muss man sich mal den Kopf der for-Schleife näher anschauen:
[...]
for (std::list<int>::const_iterator i=getBla().begin(); i!=getBla().end(); ++i) {
[...]
Im rot markierten Teil wird der Iterator i initialisiert und zeigt auf das erste Element der von getBla() zurückgelieferten Liste – einer Kopie der Originalliste, die durch die Rückgabe angefertigt wird. Im blau markierten Teil wird der Iterator vor jedem Schleifendurchlauf mit dem Endezeiger der von getBla() zurückgelieferten Liste verglichen – einer anderen Kopie, die ebenfalls wieder durch die Rückgabe by value entsteht. Nun, da der Iterator ursprünglich von der ersten Kopie an hochzählt, kann er nie den Endezeiger der zweiten erreichen, der komplett woanders liegt. Also bricht die Schleife nicht ab, wie es korrekt wäre, sondern läuft ein zweites mal durch. Durch das Hochzählen zeigt der Iterator nun hinter das Ende der ersten Kopie, per Definition also ins Nichts. *BUMM*
Ändert man den Rückgabetype von getBla() auf eine Referenz, so bekommt man bei jedem Aufruf dieselbe Liste (nämlich bla selbst). Und siehe da, dann funktioniert's! Also merke: Der Rückgabewert eines Getters ist grundsätzlich eine konstante Referenz!*grmpf* (Konstant übrigens deswegen, damit einem nicht irgendein Genie plötzlich die eigenen Daten unterm Hintern wegändert.)