10.07.2012
Version: 1
Inhaltsverzeichnis
1. Überladen mit "const"
Die genauen Überladen-Regeln mit allen Typ-Konvertierungs-Varianten sind sehr komplex, daher wollen wir uns heute nur einen kleinen Ausschnitt aus diesem großen Thema anschauen - das Überladen mit "const".2. Überladen
Überladen heißt, dass der Compiler bei einem Funktions-Aufruf anhand der Typen der Argumente deduzieren kann, welche exakte Funktion er aufrufen soll. In vielen Fällen ist dies sehr einfach:
void fct(bool);
void fct(int);
void fct(double);
fct(true); // Aufruf von "fct(bool)", da "true" ein "bool" ist
fct(23); // Aufruf von "fct(int)", da "23" ein "int" ist
fct(3.14); // Aufruf von "fct(double)", da "3.14" ein "double" ist
Wir beschränken uns heute auf den kleinen Ausschnitt vom Überladen, bei dem das "const" eine Rolle spielt.
2.1 Const-Referenz Funktions-Parameter
Beginnen wir dazu mit einer einzelnen (d.h. nicht überladenen) Funktion, die einen Parameter per Const-Referenz erwartet, und dann mit einem Objekt aufgerufen wird - eine typische Situation aus der täglichen Praxis:
#include <iostream>
#include <string>
using namespace std;
void fct(const string& s)
{
cout << "Const-String-Referenz: " << s << endl;
}
int main()
{
string s("Hallo");
fct(s); // Triviale Wandlung mit semantischer Aenderung
}
Ausgabe:
Const-String-Referenz: Hallo
- Die triviale Wandlung ist das Binden des Objekts an eine Referenz, und
- die semantische Wandlung ist der Übergang von einem Non-Const Objekt auf eine Const-Referenz - es ist ein "const" hinzugekommen.
#include <iostream>
#include <string>
using namespace std;
void fct(const string& s)
{
cout << "Const-String-Referenz: " << s << endl;
}
int main()
{
const string s("Hallo");
fct(s); // Triviale Wandlung ohne semantische Aenderung
}
Ausgabe:
Const-String-Referenz: Hallo
Genauso ist es natürlich mit einer Funktion mit Non-Const-Referenz-Parameter und Non-Const-Objekt als Argument - auch das Binden eines Objekts an eine Referenz ist eine triviale Wandlung ohne semantische Änderung:
#include <iostream>
#include <string>
using namespace std;
void fct(string& s) // Ohne "const"
{
cout << "Const-String-Referenz: " << s << endl;
}
int main()
{
string s("Hallo"); // Ohne "const"
fct(s); // Auch eine triviale Wandlung ohne semantische Aenderung
}
Ausgabe:
Const-String-Referenz: Hallo
#include <iostream>
#include <string>
using namespace std;
void fct(string& s) // Ohne "const"
{
cout << "Const-String-Referenz: " << s << endl;
}
int main()
{
const string s("Hallo"); // Mit "const"
fct(s); // Compiler-Fehler (*)
}
2.2 Triviale Wandlungen in der Typ-Konvertierungs-Hierarchie
In der Hierarchie für Typ-Konvertierungen von Argumenten während eines Funktions-Aufrufs sind die trivialen Wandlungen mit bzw. ohne semantische Änderung Konvertierungen auf zwei unterschiedlichen Hierarchie-Stufen, d.h. man kann sie für das Überladen nutzen.
#include <iostream>
#include <string>
using namespace std;
void fct(const string& s)
{
cout << "Const-String-Referenz: " << s << endl;
}
void fct(string& s)
{
cout << "String-Referenz: " << s << endl;
}
int main()
{
const string cs("Konstanter String");
string s("Veraenderbarer String");
fct(cs); // -> fct(const string&)
fct(s); // -> fct(string&)
}
Ausgabe:
Const-String-Referenz: Konstanter String
String-Referenz: Veraenderbarer String
2.3 Und wie ist das bei Zeiger-Parametern?
Genauso. Auch bei Zeiger-Parametern kann "const" zum Überladen genutzt werden.
#include <iostream>
using namespace std;
void fct(const int* pci)
{
cout << "Const-Zeiger: " << *pci << endl;
}
void fct(int* pi)
{
cout << "Zeiger: " << *pi << endl;
}
int main()
{
const int cn = 23;
int n = 42;
fct(&cn); // -> fct(const int*)
fct(&n); // -> fct(int*)
}
Ausgabe:
Const-Zeiger: 23
Zeiger: 42
2.4 Und funktioniert das auch mit call-by-value Parametern?
Das funktioniert nicht, denn bei call-by-value ("cbv") ist das "const" keine Schnittstellen-Eigenschaft des Parameters, sondern nur eine interne Eigenschaft der Funktion - aber das ist eine ganz andere Geschichte, die in einem anderen Artikel [1] beschrieben ist. Für uns ist hier nur wichtig, dass das "const" bei "cbv" keinen Einfluss auf die Signatur der Funktion hat, und daher nicht z.B. zum Überladen genutzt werden kann.
// Dies sind zwei Funktions-Deklarationen der selben Funktion,
// d.h. dies ist kein Ueberladen.
void fct(string s); // call-by-value
void fct(const string s); // call-by-value
3. Element-Funktionen mit "const" überladen
Eine identische Geschichte - und doch für C++ Anfänger oft verwunderlich - ist das Überladen von Element-Funktionen mit "const". Aber auch das ist möglich:
#include <iostream>
using namespace std;
class A
{
public:
void fct();
void fct() const;
};
void A::fct()
{
cout << "A::fct()" << endl;
}
void A::fct() const
{
cout << "A::fct() const" << endl;
}
int main()
{
A a;
const A ca;
a.fct(); // -> A::fct()
ca.fct(); // -> A::fct() const
}
Ausgabe:
A::fct()
A::fct() const
Im Prinzip sehen die beiden Element-Funktionen "fct" in der Klasse "A" unter der Haube ja folgendermaßen aus:
// Achtung - Pseudo-Code, kein echter C++ Code
class A
{
public:
pseudo-c++ void fct(A* this); // "this" ist der unsichtbare Objekt-Parameter
pseudo-c++ void fct(const A* this) const; // "this" ist der unsichtbare Objekt-Parameter
};
pseudo-c++ void A::fct(A* this) // "this" ist der unsichtbare Objekt-Parameter
{
}
pseudo-c++ void A::fct(const A* this) const // "this" ist der unsichtbare Objekt-Parameter
{
}
int main()
{
A a;
const A ca;
pseudo-c++ A::fct(&a); // Die Adresse von "a" wird als "this" mitgegeben
pseudo-c++ A::fct(&ca); // Die Adresse von "ca" wird als "this" mitgegeben
}
An diesem Pseudo-Code kann man sich gut klar machen, dass im Hintergrund quasi das normale Zeiger-Überladen zum Tragen kommt, und daher das Überladen von Element-Funktionen über das "const" der Element-Funktion sich nahtlos in das Überladen von freien Funktionen einfügt.
3.1 Wird das denn auch genutzt?
Gibt es denn auch eine praktische Anwendung für Const-Überladen? Oder ist das nur so eine Spielerei in C++ für fiese Prüfungsfragen und komische Artikel?Nein, das ist nicht nur eine Spielerei. Darum möchte ich Ihnen drei typische Anwendungen für diesen Effekt vorstellen.
3.2 Beispiel 1 - Getter-Funktionen mit "const" überladen
Eine typische Situation in der Praxis ist eine Klasse mit einem Vektor-Attribute, und einer Zugriffs-Funktion (einem Getter) für den Vektor. Da der Vektor eine Klasse ist, geben wir ihn natürlich nicht per Kopie sondern per Referenz zurück. Und da der Getter nicht verändernd sein soll, ist er "const" und damit auch die Rückgabe eine Const-Referenz.
class A
{
public:
const vector<int>& getVector() const { return v; }
private:
vector<int> v;
};
const A ca;
ca.getVector(); // Okay
A a;
a.getVector(); // Geht auch - dank trivialer Wandlung mit semantischer Aenderung fuer "a"
Was aber, wenn Sie auch gerne einen schreibenden Zugriff auf den Vektor hätten? muss er komplett neu gesetzt werden, so kann man einen normalen Setter schreiben:
class A
{
public:
void setVector(const vector<int>& arg) { v = arg; }
const vector<int>& getVector() const { return v; }
private:
vector<int> v;
};
A a;
vector<int> v = a.getVector(); // (*)
v.push_back(42);
a.setVector(v); // (**)
Eine typische Lösung, die man in der Praxis findet, ist ein zweiter überladener Non-Const-Getter, der dann auch das Attribute als Non-Const-Referenz zurückgibt.
class A
{
public:
vector<int>& getVector() { return v; } // Keine "const's"
const vector<int>& getVector() const { return v; }
private:
vector<int> v;
};
A a;
a.getVector().push_back(42); // Non-Const-A => Non-Const-Getter
// => Non-Const-Vektor-Referenz => Aenderung moeglich
const A ca;
ca.getVector().push_back(42); // Const-A => Const-Getter
// => Const-Vektor-Referenz => Compiler-Fehler
Hinweis - die überladenen Funktion "getVector" haben unterschiedliche Rückgabe-Typen. Dies ist aber kein Problem. Überladen wird über den Namen und die Parameter-Liste - der Rückgabe-Typ geht nicht ein. Und auch wenn zwei überladene Funktionen eine gewisse Nähe haben, so sind es doch vollkommen eigenständige Funktionen und können beliebig definiert werden.
Hinweis - ja, ich weiss. Wirklich schön und sauber ist so ein Non-Const-Getter nicht. Er verletzt viele grundlegende Prinzipien sauberen Codes - z.B. kann man nun den Zugriff auf das Attribute nicht mehr regeln, die Implementierung kann nicht geändert werden, uvm. Ich weiss das, und ich hoffe, mein persönlicher Code sieht anders aus. Aber - und das werden Sie zugeben müssen - in der Praxis finden wir solche Non-Const-Getter extrem viel, da sie so schön einfach und pragmatisch sind.
3.2.1 Beide Varianten sind notwendig
Mir ist hier noch einmal der Hinweis wichtig, dass - wenn man diesen Weg geht - natürlich beide Getter ihre Berechtigung haben. Ich bin immer wieder gefragt worden, wenn doch beide Getter das Gleiche machen (die gleiche Implementierung haben) - kann man dann nicht einfach einen von beiden weglassen?Nein, das geht nicht - beide Getter sind notwendig:
- Den Const-Getter benötigt man für konstante Objekte - siehe folgendes Beispiel.
- Den Non-Const-Getter benötigt man für den "schreibenden" Zugriff.
class A
{
public:
void fct(); // Achtung, "fct" ist eine non-const Funktion
};
void f(const A& a) // "A" ist Klasse => Uebergabe per Referenz
{ // Objekt soll nicht geaendert werden => Const-Referenz
a.fct(); // Compiler-Fehler, da konstantes Objekt, aber nur non-const Fkt.
}
3.3 Beispiel 2 - Iterator-Getter mit "const" überladen
Es gibt aber auch Getter, bei denen das Const-Überladen ohne Einwand sinnvoll ist - nämlich dann, wenn die Rückgabe sich eben nicht nur um ein einfaches "const" unterscheidet. Ein repräsentatives Beispiel sind die Iterator-Getter bei den STL Containern.Vielleicht ist es Ihnen noch gar nicht aufgefallen, aber z.B. den Iterator-Getter "begin()" gibt es z.B. bei der Liste doppelt - einmal mit "const" und einmal ohne. Die Rückgabe ist einmal ein "list<T>::const_iterator" und beim anderen ein "list<T>::iterator". Das sind zwei unterschiedliche Typen, die sich nicht nur durch ein "const" unterscheiden - selbst wenn das die Semantik der beiden Iterator-Typen ist.
#include <list>
using namespace std;
int main()
{
list<int> l;
const list<int> cl;
// Non-Const-Getter liefert Non-Const-Iterator
list<int>::iterator it1 = l.begin();
// Non-Const-Getter liefert Non-Const-Iterator, der sich in Const-Iterator wandeln laesst
list<int>::const_iterator it2 = l.begin();
// Const-Getter liefert Const-Iterator
list<int>::const_iterator it3 = cl.begin();
// Const-Getter liefert Const-Iterator, ohne moegliche Wandlung in Non-Const-Iterator
list<int>::iterator it4 = cl.begin(); // Compiler-Fehler
}
Hinweis - haben Sie sich gewundert? Warum nimmt der nur eine Liste und nicht z.B. einen Vektor? Der Vektor ist doch das typische Arbeitspferd in der STL, und wird eigentlich immer als STL-Container Beispiel genommen. Das hat seine Gründe. Beim Vektor arbeitet eine häufige Implementierung der Iteratoren mit Zeigern - und dann wäre der Unterschied zwischen Non-Const- und Const-Iteratoren eben doch wieder nur das "const". Und bei z.B. einem Set gibt es keinen Non-Const-Iterator, da in einem Set das Objekt ja selber für die Sortierung steht, und Änderungen über den Iterator könnten dann die Sortierung zerstören. Also eben eine Liste, wo der Effekt wirklich auftritt.
3.4 Beispiel 3 - Index-Operator [ ] mit "const" überladen
Auch die dritte typische Anwendung vom Const-Überladen haben Sie sicher schon häufig genutzt - den Index-Operator "[ ]" z.B. beim String.
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1("Hallo");
const string cs2("Welt");
char c1 = s1[1]; // Non-Const String => Non-Const Index-Operator []
char c2 = cs2[1]; // Const String => Const Index-Operator []
cout << c1 << c2 << endl;
}
Ausgabe:
ae
Da in beiden Fällen das n-te Zeichen zurückgegeben wird, könnte man das noch mit einer einzigen Const-Element-Funktion erschlagen - nur ist in "string" der Index-Operator doppelt vorhanden (als Const und als Non-Const-Variante), von daher entsprechen die obigen Kommentare der Realität.
Schauen wir uns mal eine mögliche Implementierung des Index-Operators an - und um die Erläuterung schön einfach zu halten - am Beispiel eines mäßig tollen Int-Arrays der festen Größe 2 - also ganz ohne dynamische Speicherverwaltung, ohne C-Arrays, ohne Templates, und ohne andere abgedrehte Dinge, und auch ohne Fehler-Behandlung für fehlerhafte Indices.
#include <iostream>
using namespace std;
class IntArray2
{
public:
IntArray2(int a=0, int b=0) : n1(a), n2(b) {}
int operator[](int idx) const { return idx==0 ? n1 : n2; }
private:
int n1, n2;
};
int main()
{
IntArray2 ia(2, 3);
cout << ia[0] << endl; // Tri. Wandlung mit sem. Aenderung => Aufruf const-Funktion
const IntArray2 cia(4, 2);
cout << cia[0] << endl; // Tri. Wandlung ohne sem. Aenderung => Aufruf const-Funktion
}
Ausgabe:
2
4
Die Klassen-Implementierung im Beispiel greift zu kurz, wenn man zusätzlich auch einen schreibenden Zugriff auf die Int-Elemente haben möchte.
#include <string>
using namespace std;
class IntArray2
{
public:
IntArray2(int a=0, int b=0) : n1(a), n2(b) {}
int operator[](int idx) const { return idx==0 ? n1 : n2; }
private:
int n1, n2;
};
int main()
{
string s("hallo");
s[0] = 'H'; // Schreibender Zugriff - wird von "string" unterstuetzt
IntArray2 ia;
ia[0] = 42; // Compiler-Fehler - unser "IntArray2" unterstuetzt das nicht
}
Damit der schreibende Zugriff auf unsere Array-Elemente funktioniert, benötigen wir eine überladene Const-Version des Index-Operators, die eine Referenz auf das Int-Element zurückgibt.
#include <iostream>
using namespace std;
class IntArray2
{
public:
IntArray2(int a=0, int b=0) : n1(a), n2(b) {}
int operator[](int idx) const { return idx==0 ? n1 : n2; }
int& operator[](int idx) { return idx==0 ? n1 : n2; }
private:
int n1, n2;
};
int main()
{
IntArray2 ia(2, 3);
ia[0] = 5; // Schreibender Zugriff auf Non-Const-Array funktioniert jetzt
const IntArray2 cia(4, 2);
cia[0] = 7; // Compiler-Fehler - Const-Array hat keinen Schreibzugriff
}
3.4.1 Der Compiler kennt keine Schreib- und Lese-Zugriffe
Dieses Kapitel ist eigentlich gar nicht notwendig, denn es bringt nichts neues - aber noch mal zur Verdeutlichung: "Der Compiler sucht die aufzurufende Funktion rein über die Funktions-Aufruf-Regeln des Überladens aus."
IntArray2 ia(2, 3);
int n1 = ia[0]; // Non-Const-Objekt => Non-Const-Operator - hier lesend genutzt (*)
ia[0] = 5; // Non-Const-Objekt => Non-Const-Operator - hier schreibend genutzt
const IntArray2 cia(4, 2);
int n2 = cia[0]; // Const-Objekt => Const-Operator - kann nur lesend genutzt werden
4. Wie vermeidet man Code-Verdopplung?
Vielleicht ist es Ihnen aufgefallen: in allen Beispielen waren die Implementierungen der Const- und der Non-Const-Funktion identisch - die Funktionen unterschieden sich neben dem "const" nur durch den Rückgabe-Typ. Das ist der Normalfall - sehr häufig haben überladene Const- und Non-Const-Funktionen die gleiche Implementierung. Nur, wenn dem so ist, dann ist das eine Code-Verdopplung - und das ist nicht gut. Änderungen müssen jetzt immer an zwei Stellen durchgezogen werden. Der Leser dieses Codes muss zweimal den gleichen Code verstehen, und auch verstehen, dass hier wirklich der gleiche Code steht und dies richtig ist.Kann man nicht mit einer Implementierung auskommen? Versuchen wir es mal:
// Non-Const Variante mit der Const-Variante implementieren - 1. Versuch
class A
{
public:
const vector<int>& getVector() const { return v; }
vector<int>& getVector() { return getVector(); } // Laufzeit-Fehler
// - ruft sich selber endlos auf
private:
vector<int> v;
};
Wir müssen also das "this" vorher auf den richtigen anderen Typ bringen. Also nochmal:
// Non-Const Variante mit der Const-Variante implementieren
class A
{
public:
const vector<int>& getVector() const { return v; }
vector<int>& getVector()
{
const A* const_this = this; // "const" an das "this" ran
const vector<int>& res = const_this->getVector(); // Const-Funktion aufrufen
return const_cast<vector<int>&>(res); // "const" vom Ergebnis wegnehmen
}
private:
vector<int> v;
};
// Const Variante mit der Non-Const-Variante implementieren
class A
{
public:
vector<int>& getVector() { return v; }
const vector<int>& getVector() const
{
A* non_const_this = const_cast<A*>(this); // "const" vom "this" wegnehmen
return non_const_this->getVector(); // Non-Const-Funktion aufrufen
} // - Ergebnis wird automatisch
// gewandelt ("const" hinzu)
private:
vector<int> v;
};
- In der zweiten Variante liegt die eigentlich Implementierung in der Non-Const-Funktion. Ändere ich dort das Objekt, so ist dies für den Compiler okay - ich bin ja in einer Non-Const-Funktion. Aus Sicht der Const-Funktion ist dies natürlich nicht nur falsch, sondern in höchstem Maße kritisch.
- Delegiere ich dagegen die Non-Const-Funktion an die Const-Funktion, so kann in der Const-Funktion keine Veränderung am Objekt geschehen, da dies der Compiler abfangen würde.
- Problem könnte hier natürlich sein, dass der Rückgabe-Wert nicht sinnvoll "non-const-castable" ist (was für ein Deutsch). Aber dann liesse sich die Non-Const-Funktion auch ohne Delegation nicht sinnvoll implementieren.
Vielleicht ist es aber noch besser, hier die Code-Verdopplung in Kauf zu nehmen und dafür auf den kritischen "const_cast" verzichten zu können - zumindest bei einfachen Implementierungen mache ich das so.
5. Fazit
Was bleibt:- Man kann "const" in Verbindung mit Referenzen oder Zeigern zum Überladen nutzen - für "call-by-value" Parameter geht das nicht.
- Das gleiche gilt für Const- und Non-Const-Element-Funktionen, die sich über das "const" überladen lassen - der Bezug ist hierbei der implizite "this" Parameter.
- In der Praxis findet man diese Überladung häufiger als man zuerst denkt.
- Ein Non-Const-Getter mit Non-Const-Referenz Rückgabe ist einfach und praktisch, aber kein guter Stil. Sie sollten ihn vermeiden.
- Der Compiler kennt keinen semantischen Lese- oder Schreibzugriff, sondern macht die Funktions-Auswahl beim Überladen nur nach den Überladen-Regeln.
- Will man die identische Implementierungen zweier überladenener Const- und Non-Const-Funktionen aufeinander abbilden, sollte die Non-Const-Funktion an die Const-Funktion delegiert werden. Man sollte sich aber darüber im Klaren sein, dass dies diskussions-würdiger Code ist.
6. Links
-
Artikel "Const call-by-value Parameter in C++" von Detlef Wilkening
- Noch nicht freigegeben
- Artikel "C++ Operator-Überladung von A bis (fast) Z" von Detlef Wilkening
-
Artikel "L- und R-Values in C++" von Detlef Wilkening
- Noch nicht freigegeben
7. Danksagung
Bedanken möchte ich mich bei folgenden Personen (in alphabetischer Reihenfolge), die diesen Artikel gegengelesen und mit vielen kleinen und großen Hinweisen dafür gesorgt haben, dass er besser ist als am Anfang:- Alexander Heckner
- Klaus Wittlich
- Ralph Habermann
- Robert Wittek
- Sven Johannsen
8. Versions-Historie
Die Versions-Historie dieses Artikels:-
Version 1
- 10.07.2012
- Initiale Version