Werbung

  1. Diese Seite verwendet Cookies. Wenn du dich weiterhin auf dieser Seite aufhältst, akzeptierst du unseren Einsatz von Cookies. Weitere Informationen

[C++] const-correctness, Tutorial und FAQ

Warum const geil ist und man es verwenden sollte.

  1. -AB-
    Programmiersprache(n):
    C/C++
    const-correctness ist eins der grundlegendsten Paradigmen in C++ (und eigentlich auch C), so dass ich das Thema eigentlich zuerst ansprechen muss. Von Java (wo es nichts direkt vergleichbares gibt) und eingefleischten C-Codern hört man sehr oft, teils scherzhaft, teils ernst gemeint, die kursiv geschrieben Kritiken und Fragen.

    Erst noch zu weiteren Informationen: Die englische Wikipedia hat einen hervorragenden Artikel dazu, für weitere Informationen siehe auch den Parashift Eintrag

    ...Konstante Variablen? Ist das nicht gegen das Konzept von Variablen?
    In gewisser Weise, ja. Aber deshalb nennt man const-Variablen ja auch Konstanten. Aber sehen wir doch lieber, was const überhaupt macht.

    Was macht const?
    Das Schlüsselwort const macht eine Variable zu einer Konstanten. In einigen Fällen bedeutet das, dass man den Wert der Variablen nach der Initialisierung nicht mehr ändern kann.

    In einigen Fällen? Soll das heißen, const ist nicht immer const?
    const ist dazu gedacht, Speicher als read-only anzusehen. Natürlich ist der Speicher aber sowohl physikalisch als auch vom Betriebssystem bereitgestellt veränderbar.
    Das einzige, was durch const limitiert wird, ist der Zugriff auf Speicher durch das spezifizierte Handle.
    Habe ich also
    Code (C++):
    Quelltext kopieren
    1. const int pi = 3;
    dann agiert pi wie ein Handle auf den Speicher, an dem die Zahl drei liegt. So ähnlich wie ein Pointer ein Handle auf Variablen ist, so ist eine Variable ein Handle für den darunterliegenden Speicher, der beschreibt, wie der Speicher interpretiert werden soll - z.B. als float oder als int - oder z.B. als const int, der zwar gelesen, aber nicht geschrieben werden kann.

    Gibt es mehrere Handles (Variablen) die denselben Speicher referenzieren, von denen einer nicht const ist, lässt sich der Wert verändern. Eine Möglichkeit ist beispielsweise eine Union:
    Code (C++):
    Quelltext kopieren
    1. union halb_const
    2. {
    3.     int non_const_int;
    4.     const int const_int;
    5. };
    Beide Variablen teilen sich einen Speicherbereich - aber es gibt zwei Handles, von denen nur einer const ist. Über const_int lässt sich der Wert nicht mehr verändern (nur bei der Initialisierung setzen) - über non_const_int kann der Wert beliebig verändert werden.

    Okay, ich habe nicht vor, eine Union mit einem const und einem nonconst member zu schreiben. Etwas praxisnäheres vielleicht?
    Dasselbe Konzept, dass das Handle const ist, funktioniert auch bei Pointern und Referenzen. Im einfachsten Fall heißt das, dass man einen const Pointer oder eine const Referenz haben kann, sich der referierte Wert aber (über ein anderes Handle) trotzdem ändern kann.

    Code (C++):
    Quelltext kopieren
    1. int value = 4;
    2. const int& c1 = value;
    3. const int* c2 = &value;
    Hier hat man zwei const Handles auf value, eine Referenz und einen Pointer - trotzdem ist value keine Konstante, da es ein Handle gibt, über den man den Wert ändern kann - die Variable value selbst, die nicht const ist.

    Eine Variable ist also nur eine Konstante, wenn alle Handles const sind?
    Richtig. Wie immer gibt es in C/C++ natürlich Ausnahmen.

    Warum sollte ich mir also das Leben schwieriger machen, und bestimmte Variablen zu Konstanten machen?
    Aus demselben Grund, aus dem wir Softwareentwickler uns überlegen, welches der richtige Datentyp für eine Variable ist. Ob eine Variable während ihrer Lebensdauer verändert werden soll, ist für den Programmablauf ebenso wichtig, ob ich einen int oder einen unsigned int verwenden möchte. Beides beschreibt dem Compiler (und dem Menschen) wie der entsprechende Speicherbereich genutzt werden soll. Pi sollte an sich ziemlich konstant sein - also definiert man es als Konstante. Sollte jemand versuchen, den Wert zu verändern, bekommt er einen Compilefehler.

    Aber dafür kann ich doch auch Makro.....
    NEIN. Makros sind nicht typisiert, und auch nicht konstant - je nachdem, von wo man einen Header inkludiert, kann etwas völlig anderes drin stehen. Ein Makro für Konstanten zu verwenden ist nicht besser, als überall den Wert als Literal hinzuschreiben.

    Also gut, Pi und e werden Konstanten. Was noch?
    Alles, was sich per Design während der Lebensdauer nicht ändern soll.
    Code (C++):
    Quelltext kopieren
    1. struct my_string
    2. {
    3.     my_string(unsigned int length); //allokiert mit new
    4.     ~my_string(); //deallokiert
    5.  
    6.     char* data;
    7.     const unsigned int length;
    8. };
    Hier ein Beispiel von Code, den man niemals schreiben sollte, denn es gibt den std::string (oder, als direktes Äquivalent das boost::array).
    Hier ist length ein konstanter Member von my_string. Da der Speicher genau einmal - nämlich bei der Initialisierung des Objektes - allokiert wird, ist die Länge des Strings während seiner gesamten Lebenszeit konstant. Also sollte man diese Information auch gleich im Feld length ablegen - ansonsten ändert jemand length, in der Absicht, die Stringlänge zu verändern, und ermöglicht so den Zugriff auf illegale Speicherbereiche.

    Ich kann meine konstanten Member im Konstruktor nicht zuweisen?!
    Natürlich nicht, konstante Variablen lassen sich nicht zuweisen. Dafür gibt es die Memberinitialisierungsliste. Der Konstruktor von my_string verwendet diese, um die Daten zu initialisieren.

    Und wann sollte ich nur bestimmte Handles in meinem Code const machen?
    Immer dann, wenn man den Lesezugriff auf Speicher erlauben will, aber nicht, dass jemand den Speicher ändert.
    Code (C++):
    Quelltext kopieren
    1. struct my_string2
    2. {
    3.     my_string2(unsigned int length); //allokiert mit new
    4.     ~my_string2(); //deallokiert
    5.  
    6.     const char* getData();
    7.     unsigned int getLength();
    8. private:
    9.     char* data;
    10.     const unsigned int length;
    11. };
    my_string2 könnte zum Beispiel intern string-pooling betreiben, oder generell als Layer dienen, um die Veränderbarkeit der Daten für den Anwender einzuschränken.
    Zugriff von extern ist jetzt nur noch per getData() möglich, was einen const char* returned - der sich nur zu einem const char dereferenzieren lässt. Der Wert der einzelnen Zeichen lässt sich also nicht ändern.

    Warum returned getData eine const Variable, getLength aber nicht?
    Der Rückgabewert von getLength wird kopiert. Der Nutzer bekommt also nur eine Kopie des Speicherinhalts zu sehen, die er natürlich beliebig verändern darf. Der char* hingegen zeigt auf den von my_string2 verwalteten Speicher, der nicht verändert werden sollte.
    Natürlich wird der Pointer auch kopiert. Wenn man nicht will, dass der Pointer auf eine andere Stelle zeigen soll, muss man einen konstanten Pointer verwenden.

    Einen konstanten Pointer? ein const ...* ist nicht konstant?
    Wie oben im Beispiel mit den Handles beschrieben, ist in einem const int* cip; lediglich das Handle auf den int konstant. Der Speicher selbst, in dem das Handle liegt, ist immer noch variabel. Das bedeutet, dass ein const int* einmal auf &a, und einmal auf &b zeigen darf - aber in beiden Fällen kann a oder b nicht verändert werden.
    Will man das Handle an sich konstant machen, muss man "mehr const schreiben:"

    Code (C++):
    Quelltext kopieren
    1. int* i; //nichts konstant
    2.  
    3. const int * cip; // (const int) pointer - ein Zeiger auf ein const int.
    4. int const * icp; // ( int const) pointer - ein Zeiger auf ein const int.
    5.  
    6. int * const ipc; (int pointer) const - ein Zeiger auf int, der const ist
    7.  
    8. //zu guter letzt
    9. const int * const ipc; ((const int) pointer) const - ein konstanter Zeiger auf const int.
    Statt konstanter Zeiger sollte man aber definitiv die Benutzung von Referenzen in Erwägung ziehen.

    Wir könnten unseren my_string so erweitern, dass der Pointer auf die Daten selbst konstant ist - da der Pointer sowieso nur während der Initialisierung gesetzt wird:

    Code (C++):
    Quelltext kopieren
    1. struct my_string3
    2. {
    3.     my_string3(unsigned int length) : data(new char[length]), length(length)
    4.     {
    5.     }
    6.     ~my_string3()
    7.     {
    8.         delete [] data;
    9.     }
    10.  
    11.     const char* getData()
    12.     {
    13.         return data;
    14.     }
    15.     unsigned int getLength()
    16.     {
    17.         return length;
    18.     }
    19. private:
    20.     const char* const data;
    21.     const unsigned int length;
    22. };
    Nochmal: So Code sollte niemand je schreiben. z.B. crasht er bei bestimmten Eingaben. my_string dient nur der Veranschaulichung desPrinzips!
    Ich habe ein const myObject, kann aber keine Methoden aufrufen?
    Der Compiler weiß nicht, ob eine Methode ein const-Objekt nicht vielleicht verändern könnte. Deshalb lassen sich normale Membermethoden auf ein konstantes Objekt nicht aufrufen.

    Zum Glück kann man - wie jedem Handle - auch Methoden sagen, dass sie das darunterliegende Objekt nicht verändern.

    Sich einen const my_string3 anzulegen ergibt Sinn - da es momentan eh keine Möglichkeit gibt, den String zu ändern, kann man ihn auch const speichern. Allerdings kann man - beim gegenwärtigen Design dann keine Methoden mehr nutzen. Die Methoden werden const gekennzeichnet, dann geht es:
    Code (C++):
    Quelltext kopieren
    1. struct my_string4
    2. {
    3.     my_string4(unsigned int length) : data(new char[length]), length(length)
    4.     {
    5.     }
    6.     ~my_string4()
    7.     {
    8.         delete [] data;
    9.     }
    10.  
    11.     const char* getData() const
    12.     {
    13.         return data;
    14.     }
    15.     unsigned int getLength() const
    16.     {
    17.         return length;
    18.     }
    19. private:
    20.     const char* const data;
    21.     const unsigned int length;
    22. };
    Versucht man jetzt, innerhalb getData oder getLength einen (non-const) Member zu verändern, führt das zu einem Compilefehler.
    Generell sollten also alle Methoden, die das Objekt nicht verändern, als const markiert werden.

    Puh, das ist mir etwas zu viel const. Wie genau ist da die Syntax?
    Alles, was unmittelbar links von const steht, wird als konstant angesehen. (Wenn links nichts steht, wird der Compiler u.U. etwas kreativ.)

    Code (C++):
    Quelltext kopieren
    1. int const pi = 3; //dasselbe wie const int - int ist const
    2. int * const ipc; //int pointer const - der pointer ist const
    3.  
    4. myObj::method() const // links von const steht die Methode, die nun const ist
    Was, wenn ich Lese-und Schreibzugriff ermöglichen will, bei einem const-Objekt aber nur Lesezugriff?
    In dem Fall gibt es die so genannte const-Überladung: Eine Methode, die const ist, ist unterschiedlich von einer, die nicht const ist - man kann also dieselben Parameter verwenden, und es wird eine Überladung erzeugt:
    Code (C++):
    Quelltext kopieren
    1. struct my_string5
    2. {
    3.     my_string5(unsigned int length) : data(new char[length]), length(length)
    4.     {
    5.     }
    6.     ~my_string5()
    7.     {
    8.         delete [] data;
    9.     }
    10.  
    11.     char* getData()
    12.     {
    13.         return data;
    14.     }
    15.    
    16.     const char* getData() const
    17.     {
    18.         return data;
    19.     }
    20.     unsigned int getLength() const
    21.     {
    22.         return length;
    23.     }
    24. private:
    25.     char* const data;
    26.     const unsigned int length;
    27. };
    Auch wenn const char* getData() und char* getData() sich auf den ersten Blick nur im Rückgabewert unterscheiden, so reicht das zusätzliche const aus, um eine unterschiedliche Methode daraus zu machen. Häufig findet an diese Praxis bei []-Operatoren - die eine Referenz zurückgeben, im const-Fall aber eine const Referenz.

    Gut, die Syntax ist klar. Aber wie genau kann const meinen Code besser machen?
    Guter Code ist Code, mit dem andere Coder ohne Schwierigkeiten arbeiten können.
    Anderen Codern (und auch sich selbst) kann man dabei helfen, keine Fehler zu machen, indem man a) die Nutzung des Codes auf bestimmte Arten einschränkt und b) die Variablen entsprechend ihrer intendierten Nutzungsweise kennzeichnet.
    Zu a) gehört, dass man Datenfelder, die der Nutzer nicht benutzen soll, durch private unerreichbar macht - aber auch, dass man Datenfelder, die der Nutzer nicht beschreiben soll, nur const "nach draußen" gibt.
    b) bedeutet u.A., dass man sinnvolle Variablennamen benutzt, aber eben auch, dass man durch ein const Keyword markiert, dass es in der Intention des Programmierers lag, diese Variable nicht zu verändern.
    Ganz nebenbei kann es dem Compiler helfen, den Code besser zu optimieren.

    Vorhin hieß es, es gäbe Ausnahmen - dass const nicht immer const ist?
    C++ wäre nicht C++, wenn man nicht jede Beschränkung irgendwie aushebeln könnte. Deshalb kann man auch const zu non-const machen. Für C-Coder der einfachste Weg ist ein Cast im C-Stil:
    Code (C++):
    Quelltext kopieren
    1. const int pi = 3;
    2. int& ref = (int&) pi;
    Das "wilde" casten im C-Stil ist daher auch potentiell gefährlich, weil man schnell mal unbedacht Informationen über die Verwendung eines Objektes wegcasten kann. Wer C++ schreibt, sollte den C-Cast gleich von Anfang an nicht verwenden - er ist nämlich außerdem noch schwer zu finden - da man weder nach Klammern noch nach dem Zieldatentypen sinnvoll suchen kann.

    Der "saubere" Weg, in C++ ein const loszuwerden, ist der const_cast, der ein const T zu einem T macht. Nach dem lässt sich wunderbar suchen. *Verwenden* sollte man den aber nur in absoluten Spezialfällen - also vermutlich überhaupt nicht.

    Dann gibt es noch einen etwas versteckteren Weg. In einer const-Methode kann man auf Member, die als mutable markiert sind, trotzdem schreibend zugreifen.
    Gedacht ist mutable für Fälle, wo der eigentliche Zugriff rein lesend erfolgt, z.B. beim Lesen von einem Stream. Trotzdem wird beim Lesen möglicherweise die Anzahl der gelesenen Zeichen inkrementiert - hier kann der Programmierer argumentieren, dass sich das Stream-Objekt an sich nicht wirklich ändert, und die entsprechende Variable als mutable markieren. Mutable Variablen sollten aber genau wie ein const_cast die absolute Ausnahme bilden, da sie möglicherweise konterintuitiv für den Nutzer wirken.
    Eine alternative Lösung wäre z.B., den Stream nur Daten bereitstellen zu lassen, und das Lesen von Reader-Klassen erledigen zu lassen, welche sich die Position im Stream und die Anzahl gelesener Zeichen merken können. (Java, dieser Punkt geht an dich.)

    Okay, also const. const const?
    Genau.

    Bei Fragen oder Anmerkungen, wenn etwas fehlt oder wer einfach nur ein bisschen quatschen will - oben unter dem Titel auf "Diskussion" gehen und schreiben.

Letzte Rezensionen

  1. German
    German
    5/5,
    Eine der wenigen guten Beschreibungen, die man im Netz findet.