Const-Methode, die * dies ohne const_cast ändert

8

Das folgende Muster ist in einem Programm entstanden, das ich gerade schreibe. Ich hoffe, es ist nicht zu kompliziert, aber es schafft ein Foo -Objekt in der const-Methode Foo::Questionable() const zu mutieren, ohne irgendeinen const_cast oder ähnliches zu verwenden. Grundsätzlich speichert Foo einen Verweis auf FooOwner und umgekehrt, und in Questionable() , Foo schafft es, sich in einer const-Methode zu ändern, indem mutate_foo() auf seinem Besitzer aufgerufen wird. Fragen folgen dem Code.

%Vor%

Ist das definiertes Verhalten? Soll Foo::data als veränderbar deklariert werden? Oder ist das ein Zeichen, dass ich Dinge falsch mache? Ich versuche, eine Art von lazy-initialisierten "Daten" zu implementieren, die nur gesetzt werden, wenn angefordert, und der folgende Code kompiliert gut ohne Warnungen, so bin ich ein wenig nervös, ich bin in UB Land.

Bearbeiten: Die const on Questionable () macht nur unmittelbare Elemente const und nicht die Objekte, auf die das Objekt zeigt oder auf die es verweist. Macht das den Code legal? Ich bin verwirrt zwischen der Tatsache, dass in Questionable() , this hat den Typ const Foo* , und weiter unten im Call-Stack, hat FooOwner legitimerweise einen nicht-Constant-Zeiger, den es verwendet, um Foo zu ändern. Bedeutet das, dass das Objekt Foo geändert werden kann oder nicht?

Edit 2: vielleicht ein noch einfacheres Beispiel:

%Vor%     
AshleysBrain 14.08.2010, 16:47
quelle

5 Antworten

25

Betrachten Sie Folgendes:

%Vor%

i ist ein Objekt und hat den Typ int . Es ist nicht cv-qualifiziert (ist nicht const oder volatile oder beides.)

Jetzt fügen wir hinzu:

%Vor%

j ist eine Referenz, die sich auf i bezieht, und k ist ein Zeiger, der auf i zeigt. (Von nun an kombinieren wir einfach "refer to" und "points to" mit "points to".)

An diesem Punkt haben wir zwei cv-qualifizierte Variablen, j und k , die auf ein nicht-cv-qualifiziertes Objekt zeigen. Dies wird in § 7.1.1 5.1 / 3 erwähnt:

  

Ein Zeiger oder eine Referenz auf einen cv-qualifizierten Typ muss nicht auf ein cv-qualifiziertes Objekt verweisen oder darauf verweisen, aber es wird so behandelt, als ob es dies tut; Ein Const-qualifizierter Zugriffspfad kann nicht zum Ändern eines Objekts verwendet werden, selbst wenn das Objekt, auf das verwiesen wird, ein nicht-konstantes Objekt ist und über einen anderen Zugriffspfad geändert werden kann. [Hinweis: cv-Qualifier werden vom Typsystem unterstützt, so dass sie nicht ohne Casting unterwandert werden können (5.2.11). ]

Dies bedeutet, dass ein Compiler respektieren muss, dass j und k cv-qualifiziert sind, obwohl sie auf ein nicht-cv-qualifiziertes Objekt zeigen. (So ​​sind j = 5 und *k = 5 illegal, obwohl i = 5 zulässig ist.)

Wir ziehen es jetzt in Betracht, die const aus diesen zu entfernen:

%Vor%

Das ist legal (siehe Kapitel 5.2.11), aber ist es ein undefiniertes Verhalten? Nein. Siehe § 7.1. 5.1 / 4:

  

Mit Ausnahme der Tatsache, dass jedes mutable deklarierte Klassenmitglied (7.1.1) geändert werden kann, jeder Versuch, ein const-Objekt während seiner Lebensdauer (3.8) zu ändern, führt zu undefiniertem Verhalten .    Schwerpunkt meiner.

Denken Sie daran, dass i nicht const ist und dass j und k beide auf i zeigen. Alles, was wir getan haben, ist dem Typsystem mitzuteilen, das const-Qualifikationsmerkmal vom Typ zu entfernen, damit wir das auf das angegebene Objekt ändern und dann i durch diese Variablen ändern können.

Das ist genau dasselbe wie:

%Vor%

Und das ist trivial legal. Wir betrachten jetzt, dass i dies stattdessen war:

%Vor%

Was ist mit unserem Code jetzt?

%Vor%

Es führt nun zu undefiniertem Verhalten , weil i ein für const qualifiziertes Objekt ist. Wir haben dem Typsystem befohlen, const zu entfernen, damit wir das angegebene Objekt ändern können, und dann ein für const qualifiziertes Objekt geändert haben. Dies ist undefiniert, wie oben zitiert.

Wieder, mehr offensichtlich als:

%Vor%

Beachten Sie, dass Sie einfach Folgendes tun:

%Vor%

ist vollkommen legal und definiert, da keine const-qualifizierten Objekte modifiziert werden; wir machen uns nur mit dem Typsystem herum.

Betrachten Sie jetzt:

%Vor%

Was macht const auf bar für die Mitglieder? Es macht den Zugriff auf sie durch einen sogenannten cv-qualifizierten Zugriffspfad . (Dies geschieht durch Ändern des Typs von this von T* const nach cv T const* , wobei cv der cv-Qualifier für die Funktion ist.)

Was sind die Membertypen während der Ausführung von bar ? Sie sind:

%Vor%

Natürlich sind die Typen irrelevant, da die Const-Qualifizierung der auf Objekte gerichtet ist, nicht die Zeiger. (Hatte k oben war const int* const , letzteres const ist irrelevant.) Wir betrachten nun:

%Vor%

Innerhalb von bar zeigen sowohl me als auch self auf eine nicht-konstante foo , also haben wir genau wie bei int i oben ein wohldefiniertes Verhalten. Hatten wir:

%Vor%

Wir hätten UB gehabt, genau wie mit const int , weil wir ein const-qualifiziertes Objekt modifizieren würden.

In Ihrer Frage haben Sie keine Konst-qualifizierte Objekte, also haben Sie kein undefiniertes Verhalten.

Und um der Autorität einen Appell hinzuzufügen, betrachten Sie den Trick const_cast von Scott Meyers, mit dem eine const-qualifizierte Funktion in einer nichtkonstanten Funktion wiederverwendet wird:

%Vor%

Er schlägt vor:

%Vor%

Oder wie es normalerweise geschrieben wird:

%Vor%

Beachten Sie, dass dies wiederum vollkommen legal und klar definiert ist. Da diese Funktion für eine nicht-konsistente Klasse foo aufgerufen werden muss, können wir die const-Qualifizierung ganz sicher vom Rückgabetyp int& boo() const entfernen.

(Es sei denn, jemand erschießt sich selbst mit einem const_cast + Aufruf.)

Zusammenfassend:

%Vor%

Ich beziehe mich auf den ISO C ++ 03 Standard.

    
GManNickG 14.08.2010, 22:16
quelle
6

IMO, Sie machen nichts technisch falsch. Vielleicht wäre es einfacher zu verstehen, ob das Mitglied ein Zeiger war.

%Vor%

const macht den Zeiger const, nicht den Zeiger .

Betrachten Sie den Unterschied zwischen:

%Vor%

Bei Referenzen, da sie sowieso nicht erneut eingefügt werden können, hat das Schlüsselwort const in der Methode keinerlei Auswirkungen auf Referenzmitglieder.

In Ihrem Beispiel sehe ich keine const-Objekte, also tun Sie nichts Schlechtes, indem Sie nur eine seltsame Lücke in der Art und Weise ausnutzen, wie const-Korrektheit in C ++ funktioniert.

    
UncleBens 14.08.2010 19:55
quelle
1

Ohne tatsächlich zu bekommen, ob es erlaubt ist / könnte, würde ich sehr dagegen raten. Es gibt Mechanismen in der Sprache für das, was Sie erreichen möchten, die keine obskuren Konstrukte schreiben müssen, die höchstwahrscheinlich andere Entwickler verwirren werden.

Sehen Sie sich das Schlüsselwort mutable an. Dieses Schlüsselwort kann verwendet werden, um Mitglieder zu deklarieren, die innerhalb von const -Mitgliedmethoden geändert werden können, da sie den wahrnehmbaren Status der Klasse nicht beeinflussen. Berücksichtigen Sie die Klasse, die mit einer Reihe von Parametern initialisiert wird, und führen Sie eine komplexe, teure Berechnung durch, die möglicherweise nicht immer benötigt wird:

%Vor%

Eine mögliche Implementierung besteht darin, den Ergebniswert als Element hinzuzufügen und für jede Menge zu berechnen:

%Vor%

Aber das bedeutet, dass der Wert in allen Mengen berechnet wird, ob es benötigt wird oder nicht. Wenn Sie das Objekt als eine schwarze Box betrachten, definiert die Schnittstelle nur eine Methode zum Festlegen der Parameter und eine Methode zum Abrufen des berechneten Werts. Der Zeitpunkt, zu dem die Berechnung durchgeführt wird, beeinflusst den wahrgenommenen Zustand des Objekts nicht wirklich - soweit der vom Getter zurückgegebene Wert korrekt ist. Daher können wir die Klasse so ändern, dass die Eingaben (anstelle der Ausgaben) gespeichert werden und das Ergebnis nur bei Bedarf berechnet wird:

%Vor%

Semantisch ist die zweite Klasse und die erste Klasse äquivalent, aber jetzt haben wir es vermieden, die komplexe Berechnung durchzuführen, wenn der Wert nicht benötigt wird, daher ist es von Vorteil, wenn der Wert nur in einigen Fällen angefordert wird. Gleichzeitig ist es ein Nachteil, wenn der Wert für das gleiche Objekt mehrfach angefordert wird: jedesmal wird die komplexe Berechnung durchgeführt, auch wenn sich die Eingaben nicht geändert haben.

Die Lösung speichert das Ergebnis zwischen. Dafür können wir das Ergebnis in die Klasse bringen. Wenn das Ergebnis angefordert wird, müssen wir es nur abrufen, wenn wir es bereits berechnet haben. Wenn wir den Wert nicht haben, müssen wir ihn berechnen. Wenn sich die Eingaben ändern, machen wir den Cache ungültig. Dies ist, wenn das mutable Schlüsselwort nützlich ist. Es teilt dem Compiler mit, dass das Element nicht Teil des wahrnehmbaren Zustands ist und als solches in einer konstanten Methode geändert werden kann:

%Vor%

Die dritte Implementierung ist semantisch äquivalent zu den beiden vorherigen Versionen, aber vermeiden Sie, den Wert neu berechnen zu müssen, wenn das Ergebnis bereits bekannt ist - und zwischengespeichert wird.

Das mutable -Schlüsselwort wird an anderen Stellen benötigt, wie bei Multithread-Anwendungen wird der Mutex in Klassen oft als mutable markiert. Sperren und Entsperren eines Mutex sind Mutationsoperationen für den Mutex: Sein Zustand ändert sich eindeutig. Nun ändert eine Getter-Methode in einem Objekt, die von verschiedenen Threads gemeinsam genutzt wird, den wahrgenommenen Zustand nicht, sondern muss die Sperre erfassen und freigeben, wenn die Operation Thread-sicher sein muss:

%Vor%

Die Getter-Operation ist semantisch konstant, auch wenn sie den Mutex ändern muss, um den Zugriff mit nur einem Thread auf das value -Member zu gewährleisten.

    
quelle
0

Das Schlüsselwort const wird nur während der Überprüfung der Kompilierungszeit berücksichtigt. C ++ bietet keine Möglichkeiten zum Schutz Ihrer Klasse gegen Speicherzugriff, was Sie mit Ihrem Zeiger / Verweis tun. Weder der Compiler noch die Laufzeit können wissen, ob der Zeiger auf eine Instanz zeigt, die Sie irgendwo const deklariert haben.

BEARBEITEN:

Kurzes Beispiel (möglicherweise nicht kompilierbar):

%Vor%

In diesem Fall könnte der Compiler entscheiden, ey, foo.datalength ist const, und der Code innerhalb der Schleife versprach, foo nicht zu ändern, also muss ich es auswerten Datenlänge nur einmal wenn ich die Schleife betrete. Yippie! Und wenn Sie versuchen, diesen Fehler zu debuggen, der höchstwahrscheinlich nur auftauchen wird, wenn Sie mit Optimierungen kompilieren (nicht in den Debug-Builds), werden Sie sich verrückt machen.

Halten Sie die Versprechen! Oder benutze Mutable mit deinen Brainzellen in höchster Alarmbereitschaft!

    
AndreasT 14.08.2010 16:58
quelle
-1

Sie haben zirkuläre Abhängigkeiten erreicht. Siehe FAQ 39.11 Und ja, das Ändern von const data ist UB auch wenn du den Compiler umgangen hast. Außerdem beeinträchtigst du stark die Fähigkeit des Compilers, zu optimieren, wenn du deine Versprechen nicht einhältst (sprich: const verletzen).

Warum ist Questionable const , wenn Sie wissen, dass Sie es über einen Anruf an seinen Besitzer ändern werden? Warum muss das Objekt im Besitz des Besitzers sein? Wenn du das wirklich wirklich tun musst, dann ist mutable der richtige Weg. Genau das ist es für logische Konstanz (im Gegensatz zu strikter Bit-Level-Konstanz).

Aus meiner Kopie des Entwurfs n3090:

  

9.3.2 Der this-Zeiger [class.this]

     

1 Im Hauptteil einer nicht statischen (9.3) Elementfunktion ist das Schlüsselwort dies ein rvalue ein prvalue-Ausdruck, dessen   Wert ist die Adresse des Objekts, für das die Funktion aufgerufen wird. Der Typ in einer Member-Funktion   einer Klasse X ist X *. Wenn die Memberfunktion als const deklariert ist, lautet der Typ const X * , wenn das Member   Funktion wird als flüchtig deklariert, der Typ ist flüchtig X * und wenn die Elementfunktion deklariert ist   const volatile, der Typ ist const volatile X *.

     

2 In einer const-Member-Funktion wird auf das Objekt, für das die Funktion aufgerufen wird, über einen const-Zugriff zugegriffen   Pfad; Daher darf eine konstante Elementfunktion das Objekt und seine nicht statischen Datenelemente nicht ändern.

[Hervorhebung meiner Meinung].

Bei UB:

  

7.1.6.1 Die cv-Qualifier

     

3 Ein Zeiger oder eine Referenz auf einen cv-qualifizierten Typ braucht nicht wirklich   zeigen oder beziehen Sie sich auf einen Lebenslauf-qualifizierten   Objekt, aber es wird so behandelt, als ob es   tut; ein Const-qualifizierter Zugriffspfad   kann nicht zum Ändern eines Objekts verwendet werden   auch wenn das Objekt referenziert ist a   nicht konstantes Objekt und kann geändert werden   durch einen anderen Zugangspfad. [   Hinweis: CV-Qualifier werden von unterstützt   das Typsystem, so dass sie nicht sein können   untergraben, ohne zu werfen (5.2.11).   -Hinweis]

     

4 Außer jeder Klasse   Mitglied erklärt veränderbar (7.1.1) kann   modifiziert, jeder Versuch, a   const Objekt während seiner Lebensdauer (3.8)   führt zu undefiniertem Verhalten.

    
dirkgently 14.08.2010 16:57
quelle