Wilkening-Online Logo

Überladen mit "const" in C++



Von Detlef Wilkening
10.07.2012
Version: 1

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

Wenn keine exakt passende Funktion vorhanden ist, kann die Funktions-Deduktion sehr kompliziert werden, da der Compiler pro Parameter sowohl beliebig viele in der Sprache definierte als auch eine benutzerdefinierte Umwandlung vornehmen darf.

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

Was den meisten C++ Anfängern nicht klar ist - auch wenn sie solchen Code jeden Tag n-mal implementieren - hier wird beim Funktions-Aufruf eine Typ-Konvertierung durchgeführt. Auf der Seite des Aufrufs haben wir ein String-Objekt als Argument - auf der Seite der Funktion eine Const-String-Referenz als Parameter. Hier wird ein "triviale Wandlung mit semantischer Änderung" durchgeführt: Aber auch mit einem Const-Objekt auf der Aufrufer-Seite wird beim Funktions-Aufruf eine Typ-Konvertierung durchgeführt:

#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

Diese Typ-Konvertierung ist eine triviale Wandlung ohne semantische Änderung, da hier nur ein Const-Objekt an eine Const-Referenz gebunden wird. Ein Objekt und eine Referenz sind eben nicht ganz gleich - nur fast - d.h. ist es eine triviale Wandlung. Aber hier gibt es keine semantische Änderung, denn semantisch sind ein Const-Objekt und eine Const-Referenz identisch.

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

Zu guter Letzt sollten wir uns noch mal klar machen, dass der Aufruf einer Non-Const-Referenz Funktion mit einem Const-Objekt natürlich nicht funktioniert:

#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 (*)
}

Und das ist gut, dass das nicht compiliert, denn hier würde man ein Const-Objekt an eine Non-Const-Referenz binden, über die dann eine Änderung möglich wäre. Dies - C++ sei Dank - produziert einen Compiler-Fehler beim Funktions-Aufruf in Zeile (*).

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

Super - das klappt ja problemlos und gut.

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

Auch hier ist die Wandlung eines Int-Zeigers zu einem Const-Int-Zeiger eine triviale Wandlung mit semantischer Änderung (das "const" kommt hinzu), und unterscheidet sich daher beim Überladen von der Non-Const-Int-Zeiger Funktion, die für den Non-Const-Zeiger exakt paßt.

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 passiert hier das gleiche Überladen wie bei der freien Funktion mit Const-Referenz bzw. Non-Const-Referenz Parameter. Denken Sie nämlich immer daran, dass es bei Element-Funktionen noch einen unsichtbaren (impliziten) Parameter gibt - das Objekt selber, das wir in der Element-Funktion über "this" ansprechen können.

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
}

In einer Non-Const-Element-Funktion ist "this" ein Non-Const-Zeiger auf das aktuelle Objekt, in einer Const-Element-Funktion ist "this" ein Const-Zeiger. Dies spiegelt sich in der Pseudo-Signatur der Funktion wider, bei der "this" einmal als Non-Const und einmal als Const-Zeiger definiert ist.

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;
};

Da der Getter "const" ist, können wir die Funktion sowohl für Const-Objekte als auch für Non-Const-Objekte nutzen.

const A ca;
ca.getVector();  // Okay

A a;
a.getVector();   // Geht auch - dank trivialer Wandlung mit semantischer Aenderung fuer "a"

Solchen Code schreiben Sie wahrscheinlich tag-täglich, und er ist auch nicht wirklich der Rede wert.

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;
};

Aber so ein Getter/Setter-Paar kann sehr teuer werden - will man z.B. nur einen Int-Wert an den Vektor anfügen:

A a;
vector<int> v = a.getVector();      // (*)
v.push_back(42);
a.setVector(v);                     // (**)

Machen Sie sich klar, hierbei wird erst ein Vektor kopiert (Zeile *), und dann noch ein Vektor zugewiesen (Zeile **). Und das kann verdammt teuer sein. Dieser Code ist also nicht überhaupt nicht gut.

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

Der große Vorteil dieser Lösung ist, dass man - da das Attribute als Referenz zurückgegeben wird - vollen Zugriff auf die komplette Schnittstelle des Attributes hat, und daher sehr einfach alles machen kann.

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: Denken Sie immer daran - selbst wenn Sie selber vielleicht wenig mit konstanten Objekten arbeiten (warum eigentlich?), so kommen sie in der Praxis extrem viel vor. Immer wenn wir ein Objekt einer Klasse an eine Funktion übergeben, und das aufrufende Objekt nicht verändern wollen, dann nimmt man "call-by-const-reference" - und schon hat man in der Funktion ein konstantes Objekt.

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.
}

Wie man es auch dreht und wendet - wenn man diesen Weg gehen will, dann benötigt man in der Praxis immer beide Funktionen - überladen via "const" ist also notwendig.

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
}

Immer dann, wenn die Rückgabe sich eben nicht nur durch ein "const" unterscheidet, sondern ein anderes Objekt zurückliefert (selbst wenn dieses nur eine zusätzliche Const-Semantik hat), dann ist Const-Überladen ohne Einschränkungen sinnvoll.

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

Vielleicht wundern Sie sich im Detail über das Beispiel, aber ich habe in beiden Fällen absichtlich den Index-Operator nur zum Lesen eines Zeichens genutzt, und den schreibenden Zugriff erstmal weggelassen - der kommt gleich.

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

Hinweis - wer sich bei Operator-Überladung schwer tut, dem empfehle ich das entsprechende Kapitel in meinen ausführlicheren Artikel über Operator-Überladung[2].

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
}

Hinweis - die Zuweisung "ia[0] = 42" ergibt einen Compiler-Fehler, da das zurückgegebene "char" ein sogenannter R-Value ist, d.h. nicht veränderbar ist. Details hierzu finden sich in meinem Artikel zu L- und R-Values[3].

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
}

Auch hier nochmal der Hinweis von oben: überladene Funktionen dürfen sich im Rückgabe-Typ unterscheiden.

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

Leider findet man immer wieder die Aussage, dass der Compiler beim Lese-Zugriff die Const-Variante nimmt, und beim Schreib-Zugriff die Non-Const-Variante. Das ist falsch! Der Compiler kennt keine semantischen Bedeutungen von Funktionen, und er versucht auch nicht sie herzuleiten. Er kennt die C++ Regeln - und die sind schon komplex genug - und wendet sie an. Nicht mehr, aber auch nicht weniger. Der Unterschied findet sich in Zeile (*) - semantisch wird hier ein Lese-Zugriff durchgeführt (die Const-Variante wäre semantisch sinnvoll), der Compiler nimmt aber trotzdem die Non-Const-Variante des Index-Operators, da der Operator für ein Non-Const-Objekt aufgerufen wird.

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;
};

So geht es nicht - der Aufruf der Funktion in der Klasse wird jetzt über das implizite "this" ausgewählt. Das ist hier non-const - darum nimmt der Compiler für den Aufruf wieder die Non-Const-Funktion und damit landen wir zur Laufzeit in einer Endlos-Rekursion.

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;
};

Das funktioniert - geht aber auch andersherum:

// 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;
};

Beide Varianten funktionieren - ich bevorzuge trotz mehr Schreibarbeit die Erste - warum? Unterm Strich halte ich beim Vergleich dieser Varianten die Abbildung der Non-Const-Funktion auf die Const-Funktion für die bessere Wahl. Aber man kann das auch anders sehen - entscheiden Sie für sich selber.

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:

6. Links

  1. Artikel "Const call-by-value Parameter in C++" von Detlef Wilkening
    • Noch nicht freigegeben

  2. Artikel "C++ Operator-Überladung von A bis (fast) Z" von Detlef Wilkening

  3. 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:

8. Versions-Historie

Die Versions-Historie dieses Artikels:
Schlagwörter: