Constness of least Surprise
Donnerstag, 6. Januar 2011 | Autor: Nico
Physikstudent Martin K.1 hat mir wieder einen netten kleinen Bug zugetragen! Hier erstmal der Codeausschnitt:
QList<qint32> countlist;
//std::swap funktioniert nicht, daher manuelles tauschen int_swap = countlist.at(i); countlist.at(i) = countlist.at(j); countlist.at(j) = int_swap; string_swap = keylist.at(i); keylist.at(i) = keylist.at(j); keylist.at(j) = string_swap;
Und die Fehlermeldung:
wortliste.cpp: In function 'void quick_sort(QList<QString>, QList<int>)': wortliste.cpp:49: error: assignment of read-only location 'countlist.QList<T>::at [with T = int](i)' wortliste.cpp:50: error: assignment of read-only location 'countlist.QList<T>::at [with T = int](j)' wortliste.cpp:52: error: passing 'const QString' as 'this' argument of 'QString& QString::operator=(const QString&)' discards qualifiers wortliste.cpp:53: error: passing 'const QString' as 'this' argument of 'QString& QString::operator=(const QString&)' discards qualifiers
Vielleicht mal ein paar Worte zum Lesen von Fehlermeldungen. Man fängt von oben an und sucht das erste Vorkommen von „error:
”. Vor „error:
” steht die Datei und die Zeile, in der der Fehler ist, dahinter die Fehlermeldung.
wortliste.cpp:49: error: assignment of read-only location 'countlist.QList<T>::at [with T = int](i)'
Auf Deutsch:
wortliste.cpp, Zeile 49: Fehler: Zuweisung (assignment) an eine schreibgeschützte Stelle (read-only location) namens 'countlist.QList<T>::at [with T = int](i)'
Einfacher gesprochen:
Du hast versucht, einem Symbol (zB. Variable oder so) mittels = einen Wert zuzuweisen, obwohl das hier verboten ist.
Exkurs: Constness
Warum sollte sowas verboten sein? Na zum Beispiel, wenn das Symbol keine Variable ist, sondern eine Konstante2:
1 = 2;
wäre das offensichtliche Beispiel. 1 ist nunmal eine Konstante. Man kann nicht einfach sagen „ ‚1’ hat ab jetzt den Wert 2.” (und selbst wenn man es könnte, würde man es wahrscheinlich nicht wollen.)
Zweites Beispiel:
const int e = 2.182818284590 // [...] e = 0;
Wir wollen, dass e in unserem Programm stets den Wert 2,182818284590 hat. Die Definition als Konstante mittels const
garantiert uns, dass das auch tatsächlich so ist. Wenn wir nun versuchen, e einen anderen Wert zuzuweisen, bspw. durch die Zeile e = 0;
, erinnert uns der Compiler „Nee, das geht nicht, du wolltest doch, dass e konstant ist!”
Das ist auch genau der Grund, weshalb man const benutzt - man beauftragt quasi den Compiler damit, sicherzustellen, dass bestimmte Objekte nicht versehentlich verändert werden3. Die Eigenschaft, konstant/const zu sein, nennt man Constness. Die korrekte Verwendung von const
nennt man Const-Correctness.
Der Bug
Zurück zum Bug. Schauen wir uns die vom Compiler genannte Zeile 49 an:
countlist.at(i) = countlist.at(j);
Wir haben oben festgestellt, dass die Fehlermeldung daherrührt, dass die linke Seite der Zuweisung read-only ist. Warum sollte das Ergebnis von countlist.at(i)
read-only sein? Was ist überhaupt das Ergebnis von countlist.at(i)
? Wir suchen zuerst die Deklaration von countlist
raus..
QList<qint32> countlist;
Eine QList
- offenbar ein Datentyp aus Qt4. Die Dokumentation von QList bzw. QList::at können wir einfach ergoogeln und finden5 diese Seite: doc.trolltech.com/latest/qlist.html
Wie in solchen Referenzen üblich, steht ganz oben eine Liste der Member, insbesondere der Funktionen der Klasse - dort finden wir auch at()
:
const T & at ( int i ) const
Auch wenn man den Rest vielleicht nicht versteht6, sieht man doch sofort, dass vorne const
steht - die Funktion gibt also eine Konstante zurück7 - und einer Konstante können wir halt keinen neuen Wert zuweisen.
Suchen wir also in der Dokumentation nach einer anderen Funktion, die sich wie at
verhält, aber den Wert nicht als const
Wert zurückgibt. Da es sich hier um eine Liste handelt, liegt nahe, wonach man Ausschau halten muss..
T & operator[] ( int i )
Voilà!8
Die anderen Fehlermeldungen im Stile von „discards qualifiers
” sind im Grunde das selbe. Wahrscheinlich ist die Meldung besser zu verstehen, wenn sich statt
keylist.at(i) = keylist.at(j);
das äquivalente
keylist.at(i).operator=(keylist.at(j));
anschaut.
Der Fehler liegt darin, dass wir den Qualifier, d.h. das Schlüsselwort „const
”, ignoriert (discard
) haben, indem wir auf einem konstanten Objekt (das Ergebnis von keylist.at(i)
) eine Funktion (operator=()
)aufgerufen haben, die dieses Objekt ändern könnte.
Wenn man dem Compiler sagen möchte, dass eine Funktion ein Objekt nicht ändert, muss man an die Funktionsdeklaration hinten ein const
hängen9. Das geht in diesem Fall natürlich nicht, weil = nunmal eine Funktion ist, die das Objekt ändern muss. Außerdem müssten wir diese Änderung im Code von Qt vornehmen, was keine gute Idee ist.
Aber egal - unseren Bug haben wir zumindest beseitigt!
Anm: Principle of least Surprise
Das Prinzip der geringsten Überraschung besagt nichts weiter, als dass man Systeme so gestalten sollte, dass der Benutzer keine Überraschungen erlebt, denn dann kann er intuitiv und flüssig mit dem neuen System arbeiten.
Qt beachtet dieses Prinzip an dieser Stelle nicht. Die Funktion at()
kennt man aus der Standardbibliothek als Methode von bspw. vector
und dort ist sie eben nicht const!10
Das macht es schwer, den Fehler zu sehen, weil man schlicht nicht damit rechnet, dass at()
eine Konstante zurückgibt!
Deswegen nun als Schlusswort:
Beim Design immer an’s Principle of least Surprise denken 😉
- Name nicht im Geringsten geändert. ↩
- = unveränderbar => read-only ↩
- insbesondere auch dass man selbst sie nicht versehentlich verändert ↩
- Erkennbar am Q vor List ↩
- Wenn man die Dokumentation nicht findet, hängt man halt an den Suchstring noch „Documentation” oder „Reference” oder „API” an. ↩
-
const T & at ( int i ) const
- Die Funktion gibt eine Referenz (&
) auf eine Konstante (const
) vom TypT
zurück. Der Name der Funktion istat
, sie nimmt einen Parameter vom Typint
und die Funktion verändert die Liste nicht (das hintereconst
) ↩ - Der Wert muss ursprünglich nicht konstant gewesen sein, es reicht wenn die Funktion so deklariert ist, dass die Rückgabe
const
sein soll. ↩ - Nicht funktionieren würde
value()
, weilvalue()
keine Referenz auf ein Element der Liste zurückgibt, sondern nur den Wert aus der Liste herauskopiert. Das ist nicht, was wir wollen. ↩ - Beispiel:
void foo(int k) const;
vgl. auch Fußnote 5 ↩ - Genauer: es stehen sowohl const als auch nicht-const-Varianten zur Verfügung. ↩