Standard C++

Const Correctness

Was ist “const correctness”?

Eine gute Sache. Es bedeutet, das Schlüsselwort const zu verwenden, um zu verhindern, dass const Objekte mutiert werden.

Wenn Sie beispielsweise eine Funktion f() erstellen möchten, die eine std::string akzeptiert, und Sie callersnot versprechen möchten, die std::string des Aufrufers, die an f() übergeben wird, nicht zu ändern, können Sie f() seinen std::string -Parameter empfangen lassen…

  • void f1(const std::string& s); // Pass durch Referenz-zu-const
  • void f2(const std::string* sptr); // Übergeben mit Zeiger-an-const
  • void f3(std::string s); // Pass by value

In den Fällen pass by reference-to-const und pass by pointer-to-const werden alle Versuche, diestd::string des Aufrufers innerhalb der f() Funktionen zu ändern, vom Compiler als Fehler gekennzeichnet kompilierzeit. Diese Prüfung wird vollständig zur Kompilierungszeit durchgeführt: Es gibt keine Laufzeit- oder Geschwindigkeitskosten für const . Im Fall von pass by value(f3()) erhält die aufgerufene Funktion eine Kopie des std::string des Aufrufers. Dies bedeutet, dass f3() seine localcopy ändern kann, die Kopie jedoch zerstört wird, wenn f3() zurückkehrt. Insbesondere kann f3() das std::string Objekt des Aufrufers nicht ändern.

Angenommen, Sie möchten eine Funktion g() erstellen, die eine std::string akzeptiert, aber Sie möchten callers wissen lassen, dass g() möglicherweise das std::string -Objekt des Aufrufers ändert. In diesem Fall können Sie g() seinenstd::string Parameter erhalten lassen…

  • void g1(std::string& s); // Übergeben von reference-to-non-const
  • void g2(std::string* sptr); // Übergeben mit Zeiger-zu-Nicht-const

Das Fehlen von const in diesen Funktionen teilt dem Compiler mit, dass sie das std::string -Objekt von thecaller ändern dürfen (aber nicht müssen). Somit können sie ihre std::string an eine der f() Funktionen übergeben, aber nur f3()(derjenige, der seinen Parameter “nach Wert” erhält) kann seine std::string an g1() oder g2() . Wenn f1() oder f2() eine der g() -Funktionen aufrufen müssen, muss eine lokale Kopie des std::string -Objekts an die g() -Funktion übergeben werden. Der Parameter für f1() oder f2() kann nicht direkt an eine der g() -Funktionen übergeben werden. Z.B.,

void g1(std::string& s);void f1(const std::string& s){ g1(s); // Compile-time Error since s is const std::string localCopy = s; g1(localCopy); // Okay since localCopy is not const}

Natürlich werden im obigen Fall alle Änderungen, die g1() vornimmt, an dem localCopy -Objekt vorgenommen, das lokal für f1() ist.Insbesondere werden keine Änderungen an dem const -Parameter vorgenommen, der als Verweis auf f1() übergeben wurde.

Wie hängt “const correctness” mit der normalen Typsicherheit zusammen?

Die Deklaration der const -ness eines Parameters ist nur eine weitere Form der Typsicherheit.

Wenn Sie feststellen, dass gewöhnliche Typensicherheit Ihnen hilft, Systeme korrekt zu machen (besonders in großen Systemen), werden Sie feststellen, dassconst Korrektheit auch hilft.

Der Vorteil der const -Korrektheit besteht darin, dass Sie nicht versehentlich etwas ändern können, von dem Sie nicht erwartet haben, dass es geändert wird. Am Ende müssen Sie Ihren Code mit ein paar zusätzlichen Tastenanschlägen (dem Schlüsselwort const ) dekorieren, mit dem Vorteil, dass Sie dem Compiler und anderen Programmierern zusätzliche wichtige semantische Informationen mitteilen — Informationen, die der Compiler verwendet, um Fehler zu vermeiden und andere Programmierer verwenden Sie als Dokumentation.

Konzeptionell können Sie sich vorstellen, dass const std::string beispielsweise eine andere Klasse als std::string ist, da der const -Variante konzeptionell die verschiedenen mutativen Operationen fehlen, die in der Nicht-const -Variante verfügbar sind. Sie können sich beispielsweise konzeptionell vorstellen, dass a const std::string einfach keinen Zuweisungsoperator+= oder andere mutative Operationen hat.

Sollte ich versuchen, die Dinge “früher” oder “später” const zu korrigieren?

Ganz am Anfang.

Back-Patching const Korrektheit führt zu einem Schneeballeffekt: Jedes const, das Sie “hier drüben” hinzufügen, erfordert, dass vier weitere “dort drüben” hinzugefügt werden.”

Füge const früh und oft hinzu.

Was bedeutet “const X* p”?

Es bedeutet, dass p auf ein Objekt der Klasse X zeigt, aber p kann nicht verwendet werden, um dieses X -Objekt zu ändern (natürlich könnte p auch NULL sein).

Lesen Sie es von rechts nach links: “p ist ein Zeiger auf ein X, das konstant ist.”

Wenn beispielsweise die Klasse X eine const -Elementfunktion wie inspect() const hat, ist es in Ordnung,p->inspect() zu sagen. Aber wenn die Klasse X eine Nicht-const -Memberfunktion namens mutate() hat, ist es ein Fehler, wenn Sie p->mutate() sagen.

Bezeichnenderweise wird dieser Fehler vom Compiler zur Kompilierungszeit abgefangen – es werden keine Laufzeittests durchgeführt. Das bedeutet, dass constIhr Programm nicht verlangsamt und Sie keine zusätzlichen Testfälle schreiben müssen, um die Dinge zur Laufzeit zu überprüfen – thecompiler erledigt die Arbeit zur Kompilierungszeit.

Was ist der Unterschied zwischen “const X* p”, “X* const p” und “const X* const p”?

Lesen Sie die Zeigerdeklarationen von rechts nach links.

  • const X* p bedeutet “p zeigt auf ein X, das const ist”: Das X -Objekt kann nicht überp geändert werden.
  • X* const p bedeutet “p ist ein const Zeiger auf ein X, das nicht const ist”: sie können den Zeiger pselbst nicht ändern, aber Sie können das X -Objekt über p ändern.
  • const X* const p bedeutet “p ist ein const Zeiger auf ein X, das const ist”: Sie können den Zeiger pselbst nicht ändern, noch können Sie das X Objekt über p ändern.

Und, oh ja, habe ich erwähnt, Ihre Zeigerdeklarationen von rechts nach links zu lesen?

Was bedeutet “const X& x”?

Es bedeutet x Aliase ein X Objekt, aber Sie können nicht ändern, dass X Objekt über x.

Lesen Sie es von rechts nach links: “x ist ein Verweis auf ein X, das const ist.”

Wenn beispielsweise die Klasse X eine const -Elementfunktion wie inspect() const hat, ist es in Ordnung,x.inspect() zu sagen. Aber wenn die Klasse X eine Nicht-const -Memberfunktion namens mutate() hat, ist es ein Fehlerwenn Sie x.mutate() sagen.

Dies ist völlig symmetrisch mit Zeigern auf const , einschließlich der Tatsache, dass der Compiler alle Überprüfungen zur Kompilierungszeit durchführt, was bedeutet, dass const Ihr Programm nicht verlangsamt und Sie keine zusätzlichen Testfälle schreiben müssen, um Dinge zur Laufzeit zu überprüfen.

Was bedeuten “X const& x” und “X const* p”?

X const& x entspricht const X& x und X const* x entsprichtconst X* x.

Einige Leute bevorzugen den const -on-the-right-Stil und nennen ihn “konsistent const” oder, mit einem von Simon Brand geprägten Begriff, “Ost const.” In der Tat kann der Stil”East const” konsistenter sein als die Alternative: Der Stil “East const” setzt das const immer rechts von dem, was es bezeugt, während der andere Stil manchmal das const auf der linken Seite und manchmal auf der rechten Seite (für const Zeigerdeklarationen und const Memberfunktionen).

Mit dem Stil “East const” wird eine lokale Variable const mit dem const auf der rechten Seite definiert:int const a = 42;. In ähnlicher Weise ist eine static -Variable, die const ist, als static double const x = 3.14; definiert.Grundsätzlich endet jedes const rechts von dem, was es bestätigt, einschließlich des const , das rechts sein muss: const Zeigerdeklarationen und mit einer const Memberfunktion.

Der Stil “East const” ist auch weniger verwirrend, wenn er mit Typaliasen verwendet wird: Warum haben foo und bar hier unterschiedliche Typen?

using X_ptr = X*;const X_ptr foo;const X* bar;

Die Verwendung des Stils “East const” macht dies klarer:

using X_ptr = X*;X_ptr const foo;X* const foobar;X const* bar;

Hier wird deutlicher, dass foo und foobar vom selben Typ sind und dass bar ein anderer Typ ist.

Der Stil “East const” ist auch konsistenter mit Zeigerdeklarationen. Kontrast zum traditionellen Stil:

const X** foo;const X* const* bar;const X* const* const baz;

mit dem “East const” -Stil

X const** foo;X const* const* bar;X const* const* const baz;

Trotz dieser Vorteile ist der const -on-the-Right-Stil noch nicht beliebt, sodass Legacy-Code tendenziell den traditionellen Stil aufweist.

Macht “X& const x” Sinn?

Nein, das ist Unsinn.

Um herauszufinden, was die obige Deklaration bedeutet, lesen Sie sie von rechts nach links: “x ist ein const Verweis auf einen X“. Aber das ist redundant – Referenzen sind immer const , in dem Sinne, dass Sie areference niemals neu setzen können, damit es auf ein anderes Objekt verweist. Nie. Mit oder ohne const.

Mit anderen Worten, “X& const x” ist funktional äquivalent zu “X& x“. Da Sie nichts gewinnen, indem Sie dasconst nach dem & hinzufügen, sollten Sie es nicht hinzufügen: Es wird die Leute verwirren — das const wird einige Leute denken lassen, dassdas X ist const, als ob Sie “const X& x” gesagt hätten.

Was ist eine “const member function”?

Eine Memberfunktion, die ihr Objekt prüft (anstatt es zu mutieren).

Eine const Memberfunktion wird durch ein const Suffix direkt nach der Parameterliste der Memberfunktion angezeigt. Memberfunctions mit einem Suffix const werden als “const member functions” oder “inspectors” bezeichnet.” Elementfunktionen ohne Suffixconst werden als “Nicht-const -Elementfunktionen” oder “Mutatoren” bezeichnet.”

class Fred {public: void inspect() const; // This member promises NOT to change *this void mutate(); // This member function might change *this};void userCode(Fred& changeable, const Fred& unchangeable){ changeable.inspect(); // Okay: doesn't change a changeable object changeable.mutate(); // Okay: changes a changeable object unchangeable.inspect(); // Okay: doesn't change an unchangeable object unchangeable.mutate(); // ERROR: attempt to change unchangeable object}

Der Versuch, unchangeable.mutate() aufzurufen, ist ein Fehler, der zur Kompilierungszeit abgefangen wurde. Es gibt keinen Laufzeitbereich oder Speedpenalty für const , und Sie müssen keine Testfälle schreiben, um dies zur Laufzeit zu überprüfen.

Die trailing const on inspect() Member-Funktion sollte verwendet werden, um zu bedeuten, dass die Methode den abstrakten (Client-sichtbaren) Status des Objekts nicht ändert. Das ist etwas anders als zu sagen, dass die Methode die “Rohbits” von theobject’s struct nicht ändert. C ++ – Compilern ist es nicht gestattet, die “bitweise” Interpretation zu übernehmen, es sei denn, sie können das Aliasing-Problem lösen, das normalerweise nicht gelöst werden kann (dh es könnte ein Nicht-const – Alias vorhanden sein, der den Status des Objekts ändern könnte). Eine weitere (wichtige) Erkenntnis aus diesem Aliasing-Problem: das Zeigen auf ein Objekt mit einem Zeiger auf const garantiert nicht, dass sich das Objekt nicht ändert.

Welche Beziehung besteht zwischen einer Return-by-reference und einer const-Member-Funktion?

Wenn Sie ein Mitglied Ihres this -Objekts als Referenz von einer Inspektormethode zurückgeben möchten, sollten Sie es mit reference-to-const (const X& inspect() const) oder nach Wert (X inspect() const) zurückgeben.

class Person {public: const std::string& name_good() const; // Right: the caller can't change the Person's name std::string& name_evil() const; // Wrong: the caller can change the Person's name int age() const; // Also right: the caller can't change the Person's age // ...};void myCode(const Person& p) // myCode() promises not to change the Person object...{ p.name_evil() = "Igor"; // But myCode() changed it anyway!!}

Die gute Nachricht ist, dass der Compiler Sie oft erwischt, wenn Sie dies falsch verstehen. Insbesondere wenn Sie versehentlich ein Mitglied Ihres this -Objekts durch eine Nicht-const -Referenz zurückgeben, wie in Person::name_evil() oben, erkennt der Compiler es häufig und gibt Ihnen einen Kompilierungsfehler beim Kompilieren der Innereien von in diesem Fall Person::name_evil() .

Die schlechte Nachricht ist, dass der Compiler Sie nicht immer erwischt: Es gibt einige Fälle, in denen der Compiler Ihnen einfach keine Fehlermeldung zur Kompilierungszeit gibt.

Übersetzung: Du musst nachdenken. Wenn Ihnen das Angst macht, finden Sie eine andere Arbeit; “Denken” ist kein Wort aus vier Buchstaben.

Denken Sie an die in diesem Abschnitt verbreitete “const -Philosophie”: Eine const -Memberfunktion darf den logischen Zustand des this -Objekts (auch bekannt als abstrakter Zustand, auch bekannt als meaningwisestate) nicht ändern (oder einem Aufrufer erlauben, ihn zu ändern). Denken Sie daran, was ein Objekt bedeutet, nicht wie es intern implementiert ist. Das Alter und der Name einer Person sind logischteil der Person, aber der Nachbar und der Arbeitgeber der Person sind nicht. Eine Inspektormethode, die einen Teil des logischen / abstrakten / bedeutungsweisen Zustands des this -Objekts zurückgibt, darf keinen Nicht-const -Zeiger (oder Verweis) auf diesen Teil zurückgeben, unabhängig davon, ob dieser Teil intern als direktes Datenelement implementiert ist physisch eingebettet in das this -Objekt oder auf andere Weise.

Was ist der Deal mit “const-overloading”?

const Überladen hilft Ihnen, const Korrektheit zu erreichen.

const Überladen liegt vor, wenn Sie eine Inspektormethode und eine Mutatormethode mit demselben Namen und derselben Anzahl und Art von Parametern haben. Die beiden verschiedenen Methoden unterscheiden sich nur dadurch, dass der Inspektor const und der Mutator nicht const ist.

Die häufigste Verwendung von const Überladen ist mit dem Index-Operator. Sie sollten im Allgemeinen versuchen, eine der zu Verwendenstandardcontainervorlagen, wie std::vector , aber wenn Sie Ihre eigene Klasse erstellen müssen, die einen subscriptoperator hat, hier ist die Faustregel: Indexoperatoren kommen oft paarweise vor.

class Fred { /*...*/ };class MyFredList {public: const Fred& operator (unsigned index) const; // Subscript operators often come in pairs Fred& operator (unsigned index); // Subscript operators often come in pairs // ...};

Der const -Indexoperator gibt eine const -Referenz zurück, sodass der Compiler verhindert, dass Aufrufer die Fred versehentlich mutieren / ändern. Der Nicht-const -Indexoperator gibt eine Nicht-const -Referenz zurück, mit der Sie Ihren Anrufern (und dem Compiler) mitteilen, dass Ihre Anrufer das Fred -Objekt ändern dürfen.

Wenn ein Benutzer Ihrer MyFredList -Klasse den Indexoperator aufruft, wählt der Compiler aus, welche Überladung basierend auf der Konstanz seines MyFredList aufgerufen werden soll. Wenn der Anrufer einen MyFredList a oder MyFredList& a hat, ruft a den Nicht-const -Indexoperator auf, und der Anrufer erhält einen Nicht-const -Verweis auf a Fred:

Angenommen, class Fred hat eine Inspektormethode inspect() const und eine Mutatormethode mutate():

void f(MyFredList& a) // The MyFredList is non-const{ // Okay to call methods that inspect (look but not mutate/change) the Fred at a: Fred x = a; // Doesn't change to the Fred at a: merely makes a copy of that Fred a.inspect(); // Doesn't change to the Fred at a: inspect() const is an inspector-method // Okay to call methods that DO change the Fred at a: Fred y; a = y; // Changes the Fred at a a.mutate(); // Changes the Fred at a: mutate() is a mutator-method}

Wenn der Anrufer jedoch einen const MyFredList a oder const MyFredList& a , ruft a den const subscriptoperator auf, und der Anrufer erhält einen const Verweis auf einen Fred . Dadurch kann der Aufrufer den Fred bei a überprüfen, verhindert jedoch, dass der Aufrufer den Fred bei a versehentlich mutiert / ändert.

void f(const MyFredList& a) // The MyFredList is const{ // Okay to call methods that DON'T change the Fred at a: Fred x = a; a.inspect(); // Compile-time error (fortunately!) if you try to mutate/change the Fred at a: Fred y; a = y; // Fortunately(!) the compiler catches this error at compile-time a.mutate(); // Fortunately(!) the compiler catches this error at compile-time}

Const Overloading für Subscript- und Funcall-Operatoren wird hier,hier, hier, hier und hier dargestellt.

Sie können natürlich auch const -overloading für andere Dinge als den Indexoperator verwenden.

Wie kann es mir helfen, bessere Klassen zu entwerfen, wenn ich den logischen Zustand vom physischen Zustand unterscheide?

Weil Sie dadurch ermutigt werden, Ihre Klassen von außen nach innen und nicht von innen nach außen zu entwerfen, was wiederum Ihre Klassen und Objekte einfacher zu verstehen und zu verwenden, intuitiver, weniger fehleranfällig und schneller macht. (Okay, das ist eine leichte Vereinfachung. Um alle if’s und’s und aber’s zu verstehen, müssen Sie nur den Rest dieser Antwort lesen!)

Lassen Sie uns dies von innen nach außen verstehen – Sie werden (sollten) Ihre Klassen von außen nach innen entwerfen, aber wenn Sie neu in diesem Konzept sind, ist es einfacher, von innen nach außen zu verstehen.

Im Inneren haben Ihre Objekte einen physischen (oder konkreten oder bitweisen) Zustand. Dies ist der Zustand, der für Programmierer leicht zu sehen und zu verstehen ist; Es ist der Zustand, der da wäre, wenn die Klasse nur ein C-style struct wäre.

Auf der Außenseite haben Ihre Objekte Benutzer Ihrer Klasse, und diese Benutzer dürfen nur public memberfunctions und friends . Wenn das Objekt beispielsweise der Klasse Rectangle mit den Methoden width(), height() und area() angehört, würden Ihre Benutzer sagen, dass diese drei alle Teil des logischen (oder abstrakten oder bedeutungsvollen) Zustands des Objekts sind. Für einen externen Benutzer hat das Rectangle -Objekt tatsächlich einen Bereich, auch wenn dieser Bereich im laufenden Betrieb berechnet wird (z. B. wenn die area() -Methode das Produkt aus width und height des Objekts zurückgibt). In der Tat, und das ist der wichtige Punkt, wissen Ihre Benutzer nicht und kümmern sich nicht darum, wie Sie eine dieser Methoden implementieren; ihre Benutzer nehmen aus ihrer Sicht immer noch wahr, dass Ihr Objekt logischerweise einen mittleren Zustand von Breite, Höhe und Fläche aufweist.

Das area() -Beispiel zeigt einen Fall, in dem der logische Zustand Elemente enthalten kann, die im physischen Zustand nicht direkt realisiert werden. Das Gegenteil ist auch der Fall: Klassen verbergen manchmal absichtlich einen Teil des physischen (konkreten, bitweisen) Zustands ihrer Objekte vor Benutzern — sie stellen absichtlich keine public -Elementfunktionen oderfriend -funktionen bereit, mit denen Benutzer lesen oder schreiben können oder sogar über diesen verborgenen Zustand Bescheid wissen. Das heißt, es gibt Bits im physischen Zustand des Objekts, die keine entsprechenden Elemente im logischen Zustand des Objekts haben.

Als Beispiel für diesen letzteren Fall könnte ein Sammlungsobjekt seine letzte Suche zwischenspeichern, in der Hoffnung, die Leistung seiner nächsten Suche zu verbessern. Dieser Cache ist sicherlich Teil des physischen Zustands des Objekts, aber dort ist es ein Internalimplementierungsdetail, das den Benutzern wahrscheinlich nicht zugänglich gemacht wird — es wird wahrscheinlich nicht Teil des logischen Zustands des Objekts sein. Zu sagen, was was ist, ist einfach, wenn Sie von außen nach innen denken: wenn die Benutzer des Sammlungsobjekts nun versucht haben, den Status des Caches selbst zu überprüfen, ist der Cache transparent und nicht Teil des logischen Zustands des Objekts.

Sollte die Konstanz meiner öffentlichen Elementfunktionen darauf basieren, was die Methode mit dem logischen oder physischen Zustand des Objekts macht?

Logisch.

Es gibt keine Möglichkeit, diesen nächsten Teil einfach zu machen. Es wird wehtun. Die beste Empfehlung ist, sich hinzusetzen. Und bitte, foryour Sicherheit, stellen Sie sicher, es gibt keine scharfen Geräte in der Nähe.

Gehen wir zurück zum Beispiel collection-object . Erinnern: es gibt eine Suchmethode, die die letzte Suche zwischenspeichert, um zukünftige Suchvorgänge zu beschleunigen.

Geben wir an, was wahrscheinlich offensichtlich ist: Nehmen wir an, dass die Lookup-Methode keine Änderungen am logischen Status des collection-Objekts vornimmt.

Also … die Zeit ist gekommen, dich zu verletzen. Bist du bereit?

Hier kommt: Wenn die Lookup-Methode keine Änderung am logischen Status des Sammlungsobjekts vornimmt, aber den physischen Status des Sammlungsobjekts ändert (es macht eine sehr reale Änderung am sehr realen Cache), sollte die Lookup-Methode const ?

Die Antwort ist ein klares Ja. (Es gibt Ausnahmen von jeder Regel, also sollte “Ja” wirklich ein Sternchen daneben haben, aber die überwiegende Mehrheit der Zeit ist die Antwort Ja.)

Hier dreht sich alles um “logisch const” über “physisch const.” Es bedeutet, dass die Entscheidung, ob eine Methode mit const dekoriert werden soll, in erster Linie davon abhängen sollte, ob diese Methode den logischen Zustand unverändert lässt, unabhängig davon (setzen Sie sich?) (möglicherweise möchten Sie sich hinsetzen), unabhängig davon, ob die Methode sehr reale Änderungen am sehr realen physischen Zustand des Objekts vornimmt.

Falls das nicht eingetreten ist oder Sie noch keine Schmerzen haben, teilen wir es in zwei Fälle auf:

  • Wenn eine Methode einen Teil des logischen Zustands des Objekts ändert, ist sie logischerweise ein Mutator. es sollte nicht const evenif (wie es tatsächlich passiert!) die Methode ändert keine physikalischen Bits des konkreten Zustands des Objekts.
  • Umgekehrt ist eine Methode logisch ein Inspektor und sollte const wenn sie niemals einen Teil des logischen Zustands des Objekts ändert, selbst wenn (wie tatsächlich passiert!) die Methode ändert physikalische Bits des konkreten Zustands des Objekts.

Wenn Sie verwirrt sind, lesen Sie es noch einmal.

Wenn Sie nicht verwirrt sind, aber wütend sind, gut: Sie mögen es vielleicht noch nicht, aber zumindest verstehen Sie es. Atme tief durch und wiederhole nach mir: “Die const ness einer Methode sollte von außerhalb des Objekts sinnvoll sein.”

Wenn Sie immer noch wütend sind, wiederholen Sie dies dreimal: “Die Konstanz einer Methode muss für die Benutzer des Objekts sinnvoll sein, und diese Benutzer können nur den logischen Status des Objekts sehen.”

Wenn du immer noch wütend bist, sorry, es ist was es ist. Saugen Sie es auf und leben Sie damit. Ja, es wird Ausnahmen geben; jede Regel hat sie. Aber in der Regel ist dieser logische const -Begriff in der Regel gut für Sie und gut für Ihre Software.

Noch eine Sache. Dies wird verrückt, aber lassen Sie uns genau sagen, ob eine Methode den logischen Zustand des Objekts ändert. Wenn Sie sich außerhalb der Klasse befinden – Sie sind ein normaler Benutzer -, hätte jedes Experiment, das Sie durchführen könnten (jede Methode oder Folge von Methoden, die Sie aufrufen), die gleichen Ergebnisse (dieselben Rückgabewerte, dieselben Ausnahmen oder fehlende Ausnahmen), unabhängig davon, ob Sie diese Suchmethode zum ersten Mal aufgerufen haben. Wenn die Lookup-Funktion ein zukünftiges Verhalten einer zukünftigen Methode geändert hat (nicht nur schneller, sondern auch das Ergebnis, den Rückgabewert und die Exception geändert hat), hat die Lookup—Methode den logischen Status des Objekts geändert – es ist ein Mutuator. Aber wenn die Lookup-Methode nichts anderes geändert hat, als vielleicht einige Dinge schneller zu machen, dann ist es ein Inspektor.

Was mache ich, wenn ich möchte, dass eine const-Member-Funktion eine “unsichtbare” Änderung an einem Datenelement vornimmt?

Verwenden Sie mutable (oder als letzten Ausweg const_cast ).

Ein kleiner Prozentsatz der Inspektoren muss Änderungen am physischen Zustand eines Objekts vornehmen, die von externen Benutzern nicht beobachtet werden können — Änderungen am physischen, aber nicht am logischen Zustand.

Beispielsweise hat das zuvor diskutierte Sammlungsobjekt seine letzte Suche in der Hoffnung zwischengespeichert, die Leistung seiner nächsten Suche zu verbessern. Da der Cache in diesem Beispiel von keinem Teil der öffentlichen Schnittstelle des Sammlungsobjekts (außer dem Timing) direkt beobachtet werden kann, sind seine Existenz und sein Status nicht Teil des logischen Status des Objekts. Die Lookup-Methode ist ein Inspektor, da sie niemals den logischen Zustand des Objekts ändert, unabhängig davon, dass sie zumindest für die vorliegende Implementierung den physischen Zustand des Objekts ändert.

Wenn Methoden den physischen, aber nicht den logischen Zustand ändern, sollte die Methode im Allgemeinen als const markiert werden, da es sich wirklich um eine Inspektormethode handelt. Das schafft ein Problem: Wenn der Compiler sieht, dass Ihre const —Methode den physischen Status des this -Objekts ändert, wird er sich beschweren – es wird Ihrem Code eine Fehlermeldung geben.

Die Compilersprache C++ verwendet das Schlüsselwort mutable, um Ihnen zu helfen, diesen logischen Begriff const zu verwenden. In diesem Fall würden Sie den Cache mit dem Schlüsselwort mutable markieren, damit der Compiler weiß, dass er innerhalb einerconst -Methode oder über einen anderen const -Zeiger oder eine andere Referenz geändert werden darf. In unserem Jargon markiert das Schlüsselwort mutable diejenigen Teile des physischen Zustands des Objekts, die nicht Teil des logischen Zustands sind.

Das Schlüsselwort mutable steht kurz vor der Deklaration des Datenelements, dh an derselben Stelle, an der Sieconst . Der andere Ansatz, der nicht bevorzugt wird, besteht darin, die const -ness des this -Zeigers wegzuwerfen, wahrscheinlich über dasconst_cast -Schlüsselwort:

Set* self = const_cast<Set*>(this); // See the NOTE below before doing this!

Nach dieser Zeile hat self die gleichen Bits wie this, dh self == this, aber self ist eine Set* und nicht eineconst Set* (technisch gesehen ist this eine const Set* const, aber die am weitesten rechts liegende const ist für diese Diskussion irrelevant).Das heißt, Sie können self verwenden, um das Objekt zu ändern, auf das this zeigt.

HINWEIS: Bei const_cast kann ein äußerst unwahrscheinlicher Fehler auftreten. Es passiert nur, wenn drei sehr seltene Dinge gleichzeitig kombiniert werden: ein Datenelement, das mutable sein sollte (wie oben beschrieben), ein Compiler, der das Schlüsselwort mutable nicht unterstützt, und / oder ein Programmierer, der es nicht verwendet, und ein Objekt, das ursprünglich als const definiert wurde (im Gegensatz zu einem normalen Nicht-const -Objekt, auf das ein Zeiger auf const zeigt).Obwohl diese Kombination so selten ist, dass sie Ihnen möglicherweise nie passiert, funktioniert der Code möglicherweise nicht (der Standard besagt, dass das Verhalten undefiniert ist).

Wenn Sie jemals const_cast verwenden möchten, verwenden Sie stattdessen mutable. Mit anderen Worten, wenn Sie jemals ein Mitglied von anobject ändern müssen und dieses Objekt von einem Zeiger auf-const , ist es am sichersten und einfachsten, mutable zur Deklaration des Mitglieds hinzuzufügen. Sie können const_cast Wenn Sie sicher sind, dass das eigentliche Objekt nicht const ist (z. B. wenn Sie sicher sind, dass das Objekt so deklariert ist: Set s; ), aber wenn das Objekt selbst könnte const sein (z. B. wenn es wie folgt deklariert sein könnte: const Set s;), verwenden Sie mutable anstelle von const_cast.

Bitte schreiben Sie nicht, dass Sie mit Version X von Compiler Y auf Maschine Z ein Nicht-mutable Mitglied einesconst Objekts ändern können. Es ist mir egal – es ist je nach Sprache illegal und Ihr Code wird wahrscheinlich auf einem anderen Compiler oder sogar einer anderen Version (einem Upgrade) desselben Compilers fehlschlagen. Sag einfach nein. Verwenden Sie stattdessen mutable. Schreiben Sie Code, der garantiert funktioniert, nicht Code, der nicht zu brechen scheint.

Bedeutet const_cast verlorene Optimierungsmöglichkeiten?

In der Theorie ja; in der Praxis nein.

Selbst wenn die Sprache const_cast verbietet, besteht die einzige Möglichkeit, das Leeren des Registercaches über einen const memberfunction Aufruf zu vermeiden, darin, das Aliasing-Problem zu lösen (dh., um zu beweisen, dass es keine Nicht-const Zeiger gibt, die auf das Objekt zeigen). Dies kann nur in seltenen Fällen passieren (wenn das Objekt im Rahmen des const Memberfunction-Aufrufs konstruiert wird und wenn alle Nicht-const -Memberfunktionsaufrufe zwischen der Konstruktion des Objekts und demconst -Member-Funktionsaufruf statisch gebunden sind und wenn jeder dieser Aufrufe ist auch inline d, undwenn der Konstruktor selbst inlined ist und wenn alle Member-Funktionen, die der Konstruktor aufruft, inline sind).

Warum erlaubt mir der Compiler, ein int zu ändern, nachdem ich mit einem const int* darauf gezeigt habe?

Weil “const int* p” bedeutet “p verspricht nicht zu ändern die *p,” nicht “*p verspricht nicht zu ändern.”

Wenn eine const int* auf eine int zeigt, wird die int nicht const -ifiziert. Das int kann nicht über dasconst int* geändert werden, aber wenn jemand anderes ein int* (Hinweis: kein const) hat, das auf (“Aliase”) dasselbe int zeigt, dann kann diesesint* verwendet werden, um das int zu ändern. Beispielsweise:

void f(const int* p1, int* p2){ int i = *p1; // Get the (original) value of *p1 *p2 = 7; // If p1 == p2, this will also change *p1 int j = *p1; // Get the (possibly new) value of *p1 if (i != j) { std::cout << "*p1 changed, but it didn't change via pointer p1!\n"; assert(p1 == p2); // This is the only way *p1 could be different }}int main(){ int x = 5; f(&x, &x); // This is perfectly legal (and even moral!) // ...}

Beachten Sie, dass sich main() und f(const int*,int*) in verschiedenen Kompilierungseinheiten befinden können, die an verschiedenen Wochentagen kompiliert werden. In diesem Fall kann der Compiler das Aliasing zur Kompilierungszeit auf keinen Fall erkennen. Daher gibt es keine Möglichkeit, eine Sprachregel zu erlassen, die so etwas verbietet. Tatsächlich möchten wir nicht einmal eine solche Regel erstellen, da es im Allgemeinen als Funktion angesehen wird, dass viele Zeiger auf dasselbe verweisen können. Die Tatsache, dass einer dieser Zeiger verspricht, das zugrunde liegende “Ding” nicht zu ändern, ist nur ein Versprechen des Zeigers; es ist kein Versprechen des “Dings”.

Bedeutet “const =* p”, dass *p sich nicht ändern kann?

Nein! (Dies bezieht sich auf die FAQ zum Aliasing von int -Zeigern.)

const Fred* p” bedeutet, dass Fred nicht über den Zeiger p geändert werden kann, aber es gibt möglicherweise andere Möglichkeiten, an theobject zu gelangen, ohne einen const (z. B. einen aliased non-const Zeiger wie einen Fred* ) . Wenn Sie beispielsweise zwei Zeiger “const Fred* p” und “Fred* q” haben, die auf dasselbe Fred -Objekt zeigen (Aliasing), kann der Zeiger q zum Ändern des Fred -Objekts verwendet werden, der Zeiger p jedoch nicht.

class Fred {public: void inspect() const; // A const member function void mutate(); // A non-const member function};int main(){ Fred f; const Fred* p = &f; Fred* q = &f; p->inspect(); // Okay: No change to *p p->mutate(); // Error: Can't change *p via p q->inspect(); // Okay: q is allowed to inspect the object q->mutate(); // Okay: q is allowed to mutate the object f.inspect(); // Okay: f is allowed to inspect the object f.mutate(); // Okay: f is allowed to mutate the object // ...}

Warum erhalte ich einen Fehler beim Konvertieren eines Foo** → const Foo** ?

Weil das Konvertieren von Foo**const Foo** ungültig und gefährlich wäre.

C++ erlaubt die (sichere) Konvertierung Foo*Foo const*, gibt jedoch einen Fehler aus, wenn Sie versuchen, Foo**const Foo** implizit zu konvertieren.

Die Gründe, warum dieser Fehler eine gute Sache ist, sind unten angegeben. Aber zuerst ist hier die häufigste Lösung: simplychange const Foo** zu const Foo* const*:

class Foo { /* ... */ };void f(const Foo** p);void g(const Foo* const* p);int main(){ Foo** p = /*...*/; // ... f(p); // ERROR: it's illegal and immoral to convert Foo** to const Foo** g(p); // Okay: it's legal and moral to convert Foo** to const Foo* const* // ...}

Der Grund, warum die Konvertierung von Foo**const Foo** gefährlich ist, besteht darin, dass Sie ein const Foo -Objekt stillschweigend und versehentlich ohne Umwandlung ändern können:

class Foo {public: void modify(); // make some modification to the this object};int main(){ const Foo x; Foo* p; const Foo** q = &p; // q now points to p; this is (fortunately!) an error *q = &x; // p now points to x p->modify(); // Ouch: modifies a const Foo!! // ...}

Wenn die Zeile q = &p legal wäre, würde q auf p zeigen. Die nächste Zeile, *q = &x , ändert p selbst (da *qpist), um auf x zu zeigen. Das wäre eine schlechte Sache, da wir das Qualifikationsmerkmal const verloren hätten: p ist ein Foo*, aberx ist ein const Foo. Die p->modify() -Zeile nutzt die Fähigkeit von p aus, ihren Referenten zu ändern, was das eigentliche Problem ist, da wir am Ende einen const Foo geändert haben.

Wenn Sie einen Verbrecher unter einer rechtmäßigen Verkleidung verstecken, kann er das Vertrauen, das dieser Verkleidung gegeben wird, ausnutzen.Das ist schlimm.

Glücklicherweise verhindert C ++ dies: Die Zeile q = &p wird vom C++ – Compiler als Compile-timeerror gekennzeichnet. Erinnerung: Bitte zeigen Sie sich nicht um diese Fehlermeldung zur Kompilierungszeit herum. Sag einfach Nein!

(Hinweis: Es gibt eine konzeptionelle Ähnlichkeit zwischen diesem und dem Verbot der Konvertierung von Derived** inBase** .)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.