2013-02-11 5 views
7

mit einem Behälterklasse wie std::vector, gibt es zwei unterschiedliche Konzepte der Konstantheit: die des Behälters (d.h. seine Größe), und dass die Elemente. Es scheint, dass std::vector diese beiden verwirrt, so dass die folgenden einfachen Code wird nicht kompiliert:const vs nicht konstanten des Behälters und dessen Inhalts

struct A { 
    A(size_t n) : X(n) {} 
    int&x(int i) const { return X[i]; } // error: X[i] is non-const. 
private: 
    std::vector<int> X; 
}; 

Beachten Sie, dass, obwohl die Datenelemente (die drei Zeiger auf das & Ende der Daten beginnen und Ende der zugewiesenen Puffers) von std::vector sindnicht durch einen Aufruf an seinen operator[] geändert, ist dieses Mitglied const nicht - ist das nicht ein seltsames Design?

Beachten Sie auch, dass für Roh-Zeigern, diese beiden Konzepte von konstant-ness sind fein säuberlich getrennt, so dass die entsprechenden Roh-Zeiger Code

struct B { 
    B(size_t n) : X(new int[n]) {} 
    ~B() { delete[] X; } 
    void resize(size_t n);     // non-const 
    int&x(int i) const { return X[i]; } // fine 
private: 
    int*X; 
}; 

funktioniert gut.

Also, was ist der richtige/empfohlene Weg, um dies zu behandeln, wenn std::vector (ohne mutable) verwendet wird?

Ist ein const_cast<> wie in

int&A::x(int i) const { return const_cast<std::vector<int>&>(X)[i]; } 

als akzeptabel erachtet (X bekannt ist, nicht const, so dass kein UB hier)?

EDIT nur weitere Verwirrung zu vermeiden: ich die Elemente ändern will, das heißt den Inhalt des Behälters, aber nicht der Behälter selbst (die Größe und/oder Speicherplatz).

+0

Die Daten von 'std :: vector' könnten durch Ihren Aufruf von' operator [] 'geändert werden. Wenn Sie 'A :: X' geschrieben haben, ist 'a.x (1) ++;' vollkommen legal und ändert den Inhalt des Vektors. –

+1

@DavidSchwartz Der * Inhalt * eines Vektors ist nicht seine tatsächliche * Daten * (obwohl Sie sie logischerweise als solche assoziieren können). Wenn Sie 'std :: vector' überprüfen, hat es nur 3 Zeiger als Daten (Anfang und Ende der Daten und Ende des Puffers). Diese bleiben unverändert. – Walter

Antwort

13

C++ unterstützt nur eine Ebene von const. Soweit der Compiler betrifft, so ist es bitweise const: die „Bits“ tatsächlich in der Objekt (dh in sizeof gezählt) kann nicht ohne Spielen geändert werden (const_cast, etc.), aber alles andere ist fair Spiel . In den frühen Tagen von C++ (Ende der 1980er, Anfang der 1990er Jahre) gab es eine Menge Diskussion über die Designvorteile von bitweisem const vs. logischem const (auch bekannt als Humpty-Dumpty const, weil, wie Andy Koenig einmal sagte , wenn der Programmierer const verwendet, bedeutet es genau, was der Programmierer will, dass es bedeutet). Der Konsens verschmolz schließlich zugunsten von logischem const.

Dies bedeutet, dass Autoren von Container-Klassen eine Wahl haben müssen. Sind die Elemente des Containerteils des Containers, oder nicht. Wenn sie Teil des Containers sind, können sie nicht geändert werden, wenn der Container const ist. Es gibt keine Möglichkeit, eine Auswahl anzubieten; der Autor des Containers muss einen oder den anderen wählen. Auch hier scheint es einen Konsens zu geben: Die Elemente sind Teil des Containers, und wenn der Container const ist, können sie nicht geändert werden. (Vielleicht ist die parallel mit C-Stil-Arrays spielte hier eine Rolle, wenn eine C-Stil-Array const ist, dann kann man nichts von seinen Elementen ändern.)

Wie Sie, ich habe mal begegnet, wenn ich verbieten wollte Änderung der Größe des Vektors (vielleicht Iteratoren zu schützen), aber nicht seiner Elemente. Es gibt keine wirklich zufriedenstellende Lösungen; das beste, was ich mir vorstellen kann, ist einen neuen Typ zu erstellen, der eine mutable std::vector enthält, und Weiterleitungsfunktionen, die der Bedeutung von const ich in diesem speziellen Fall entsprechen. Und wenn Sie drei Ebenen unterscheiden möchten (vollständig const, teilweise const und nicht-const), Sie Ableitung benötigen. Die Basisklasse exponiert nur die vollständig const und teilweise const Funktionen (z. B. eine const int operator[](size_t index) const; und int operator[]( size_t index);, aber nicht void push_back(int);); Die Funktionen, die das Einfügen und Entfernen eines Elements ermöglichen, sind nur in der abgeleiteten Klasse ausgesetzt. Clients, die keine Elemente einfügen oder entfernen sollen, erhalten nur einen nichtkonstanten Verweis an die Basisklasse.

+0

+1 nice Diskussion. Ich bin jedoch nicht davon überzeugt, dass der Designer einer Containerklasse keine Wahl hat. Man könnte eine Container-Klasse implementieren, die die Const-ness ihrer Elemente als Template-Argument enthält und die Move-Conversion (unter Verwendung von Smartpointern) zwischen Const- und Nicht-Const-Containern erlaubt. Ihr von der Basis abgeleitetes Design sieht jedoch einfacher aus. – Walter

+0

@Walter Der Designer einer Containerklasse hat alle möglichen Optionen. Der Designer von 'std :: vector' hat in diesem Fall einen gemacht, für den es einen allgemeinen Konsens zu geben scheint - die meisten der vordefinierten Container, die mir bekannt sind, machten das gleiche. Global scheint es keine große Nachfrage nach einem Container zu geben, der die drei Ebenen von "const" (keine, teilweise oder vollständig) anbietet, außer in speziellen Fällen (z. B. "std :: array"). (In Tagen vor dem Standard nahm eine meiner Array-Klassen die Größe als Konstruktorargument und bot keine Möglichkeit, sie später zu ändern.) –

+0

@JamesKanze: Die Erklärung ist ausgezeichnet, aber die ersten zwei Eröffnungssätze ganz oben gaben an der Eindruck, dass bitweises const das ist, was C++ unterstützt. * (Ich hoffe, es gibt nicht zu viele C++ - Programmierer, die TL sind; DR.) * Dieser einleitende Satz ist auf dem Compiler-Backend wahr, aber das Frontend betrachtet 'const' nur als syntaktische Hilfe - das ist es in der Typprüfung verwendet, das ist es. (Übrigens, C++ 11 führt die Kompilierungszeit 'constexpr' ein.) Logische Konstanz existiert nur in den Gehirnen von Programmierern. Ein C++ - Programmierer wird die Unterschiede erkennen, wenn man Multi-Thread-Programmierung beginnt. – rwong

3

Es ist nicht eine seltsame Design, es ist eine sehr bewusste Entscheidung, und die richtige IMHO.

struct C { 
    int& get(int i) const { return X[i]; } 
    int X[N]; 
}; 

aber mit dem sehr nützlichen Unterschied, dass die Anordnung der Größe verändert werden kann:

Ihr B Beispiel für ein std::vector keine gute Analogie ist, wäre eine bessere Analogie sein. Der obige Code ist ungültig aus dem gleichen Grunde wie das Original ist, das Array (oder vector) Elemente sind vom Konzept „Mitglieder“ (technisch Unterobjekte) des enthaltenden Typs, so dass Sie nicht in der Lage sein sollten, sie durch ein const Mitglied zu ändern Funktion.

Ich würde sagen, die const_cast ist nicht akzeptabel, und keiner verwendet mutable, es sei denn als letztes Mittel. Sie sollten fragen, warum Sie die Daten eines const-Objekts ändern möchten, und in Betracht ziehen, die Elementfunktion nicht-const zu machen.

+0

Dies ist eine Frage der Interpretation. Wenn Sie einen 'Vektor' als ein C-Array betrachten, das skalierbar ist, stimme ich zu. Wo passen aber die Funktionen 'resize()' usw. zu dieser Analogie? Sie müssen mehr als konstant sein: Sie möchten nicht-konstanten Zugriff auf die Elemente geben können, aber nicht auf die Größe. Dies ist mit 'std :: vector' nicht möglich. Ich denke, die übliche Art, dies zu lösen, besteht darin, Iteratoren bereitzustellen, die typischerweise erlauben, den Inhalt zu ändern, aber nicht den Container. – Walter

+0

Es ist nur eine Analogie, nehmen Sie es nicht zu wörtlich, sowieso 'resize' ist nicht const, so ist es erlaubt, das Objekt zu mutieren. Sie sind sich nicht sicher, was Sie mit Iteratoren meinen, aber keiner der Standardcontainer gibt Ihnen einen nichtkonstanten Iterator für einen Const-Container. –

+0

Was ich mit Iteratoren meinte, ist, dass Sie den Container nicht ändern können, wenn Sie einen (nichtkonstanten) Iterator haben. Der Container bleibt also unverändert, während die Elemente geändert werden können. – Walter

4

Leider anders als Zeiger, können Sie so etwas wie

std::vector<int> i; 
std::vector<const int>& ref = i; 

nicht tun, deshalb, std::vector kann nicht zwischen den beiden Arten von const disambiguate wie sie angewendet werden könnte, und es hat konservativ.Ich persönlich würde, wählen Sie so etwas wie

const_cast<int&>(X[i]); 

bearbeiten tun: Wie ein anderer Kommentator genau darauf hingewiesen, Iteratoren tun Modell diese Dichotomie. Wenn Sie eine vector<int>::iterator an den Anfang gespeichert haben, können Sie sie in einer const-Methode de-referenzieren und eine nicht-const int& zurückgeben. Ich denke. Aber Sie müssen auf die Ungültigkeit achten.

+1

Die Verwendung von Iteratoren schlägt eine interessante Lösung vor: eine _view_-Klasse, die nur den Anfangs- und Ende-Iterator enthält und nur die eingeschränkte Funktionalität bereitstellt, die er möchte. (In diesem Fall würde auch eine View-Klasse mit einem Zeiger auf den Vektor funktionieren.) –

-1

Ich würde vorschlagen, std::vector::at() Methode anstelle einer const_cast.

+0

Die const-Überladung von 'vector :: at()' gibt eine 'const'-Referenz zurück, so dass das nicht hilft –