__property Schlüsselwort und const-correctness
-
Hallo zusammen,
ich versuche grad mein Projekt vom BCB6 auf das Codegear RAD Studio 2007 zu portieren und stosse dabei auf gewaltige Probleme. Was unter BCB6 noch tadellos funktionierte führt beim CG2007 zu Compilerfehlern. Leider taucht dieses Problem an hunderten von Stellen auf, sodass ich gern eine generelle Lösung für das Problem hätte, statt überall Workarounds einzubauen.
Hier ist der minimal Code, der das Problem reproduziert (Codegear RAD Studio 2007, Version 11.0.2804.9245):#include <vector> #pragma option -w-8004 struct T { std::vector<int> v_; T() : v_( 1,0 ) {}; __property std::vector<int> V = { read=v_, write=v_ }; }; void func( const T& t ) { int i1 = t.V[0]; // Fehler (Zeile 16) int i2 = (t.V)[0]; // OK ?! (Zeile 17) } int main(int argc, char* argv[]) { T t; func( t ); return 0; }
Die Fehlermeldung lautet:
`[C++ Fehler] main.cpp(16): E2522 Nicht-konstante Funktion operator std::vector<int,std::allocator<int> >::[](unsigned int) für konstantes Objekt aufgerufen
`
Wenn ich den Ausdruck klammere läuft der Compiler durch (siehe Zeile 17). Nur möchte ich nicht überall diese Ausdrücke klammern, weil´s zum ersten sehr viele Vorkommen sind (>200) und zweitens der Code dadurch nicht leserlicher wird.
Bin, wie immer, dankbar für jeden Hinweis.
-
Nächstes Problem (sind zwar nur Warnungen, aber trotzdem):
Fast alle VCL Funktionen/Properties, die eine Zahl zurückgeben, geben einen int zurück. Da ich ints nur dann benutze, wenn negative Zahlen zum Wertebereich gehören, aber sonst unsigned int, hagelt es Warnungen, weil ich int mit unsigned int vergleiche. Ausser die Warnungen auszuschalten ist mir da auch noch nix eingefallen.
-
DocShoe schrieb:
Hier ist der minimal Code, der das Problem reproduziert (Codegear RAD Studio 2007, Version 11.0.2804.9245):
#include <vector> #pragma option -w-8004 struct T { std::vector<int> v_; T() : v_( 1,0 ) {}; __property std::vector<int> V = { read=v_, write=v_ }; }; void func( const T& t ) { int i1 = t.V[0]; // Fehler (Zeile 16) int i2 = (t.V)[0]; // OK ?! (Zeile 17) } int main(int argc, char* argv[]) { T t; func( t ); return 0; }
Die Fehlermeldung lautet:
`[C++ Fehler] main.cpp(16): E2522 Nicht-konstante Funktion operator std::vector<int,std::allocator<int> >::[](unsigned int) für konstantes Objekt aufgerufen
`
Um den Fehler nur einfach schnell loszuwerden, deklariere v_ als mutable.
Offenbar existiert im Compiler Code, der den []-Operator, wenn er auf ein Property appliziert wird, das gar kein indiziertes ist, explizit als []-Operator mit dem Ergebnis des Property-Getters als Argument neuinterpretiert - und hierbei leider ein kleines Problem mit der const-correctness übersieht. In anderen Fällen, z.B. in deiner Klammerung oder beim Aufruf des Operators mittels
t.V.operator [] (0);
gibt es diese Probleme nicht.
Könntest du dazu einen QC-Report erstellen?Allerdings möchte ich noch darauf hinweisen, daß diese Verwendung eines Properties nicht ratsam ist. Durch eine (hier in aller Ausführlichkeit diskutierte) Nachlässigkeit des Compilers ist es beispielsweise, sofern als Getter eine Membervariable angegeben wird, möglich, Dinge wie
T t; t.V.push_back (1);
zu machen - damit gibt man aber ein Implementationsdetail preis und kann das Property gleich weglassen. Verwendet man aber eine Getter-Funktion, so gibt diese ein temporäres Objekt zurück, für das die Funktion dann aufgerufen wird, und v_ bleibt unberührt.Properties sind nur für die Rückgabe von Wertetypen gedacht (worunter auch Zeiger fallen). Fragwürdigerweise ist zwar auch std::vector<> ein solcher, und deshalb funktioniert das auch, aber die Verwendung einer Getter-Funktion führt in der Regel zur Erstellung einer Kopie, und das ist nur bei tatsächlichen Wertetypen unproblematisch.
DocShoe schrieb:
Fast alle VCL Funktionen/Properties, die eine Zahl zurückgeben, geben einen int zurück. Da ich ints nur dann benutze, wenn negative Zahlen zum Wertebereich gehören, aber sonst unsigned int, hagelt es Warnungen, weil ich int mit unsigned int vergleiche. Ausser die Warnungen auszuschalten ist mir da auch noch nix eingefallen.
Da finde ich den Sachverhalt eindeutig. Wenn du die Warnung nicht willst, deaktiviere sie.
-
Ok, habe gestern einen Bug Report erstellt, vielleicht wird der tatsächlich irgendwann mal gefixt. Bis dahin werde ich Container mit operator[] nicht mehr als __property verwenden, bzw. den Zugriff über at() machen.
In der Borland Hilfe finde ich nirgends Hinweise über Einschränkungen bei der Benutzung von __property, und daher sollte es mit jedem Datentyp gehen. Wenn __property nur für Wertetypen und Zeiger sein soll, dann wäre es ein Leichtes gewesen, das zur compile time zu prüfen und entsprechende Fehler anzuzeigen.
Natürlich gebe ich Implementationsdetails preis, aber das tue ich mit jeder Property, die Objekte zurückgibt. Selbst wenn ich Zeiger auf Objekte zurückgebe tue ich das, selbst wenn es Zeiger auf Interfaces sind.
In der VCL gibt es zuhauf Zeiger auf TStringList (oder deren Interfaceklasse TStrings) , die per __property zurückgegeben werden und per AddString() modifiziert werden können. Um das Dilemma in den Griff zu bekommen benutzt Borland das OnChange Ereignis, damit die umgebende Klasse Änderungen an der StringList mitbekommt. Das ist IMHO ein Workaround für dieses Problem, wenn man das wirklich sauber lösen möchte muss man wohl für jede nutzbare TStringList Methode eine Proxy Methode in der umgebenden Klasse bereitstellen, was natürlich total blödsinnig weil zu aufwändig ist. Daher sehe ich es als durchaus sinnvoll an, Referenzen auf member zurückzugeben. Ausserdem führen Objektkopien bei grossen Objekten zu Performance Hits, z.B. wenn ein deque 100.000 Elemente hat, die bei jedem getter Aufruf kopiert werden müsste.
-
DocShoe schrieb:
In der Borland Hilfe finde ich nirgends Hinweise über Einschränkungen bei der Benutzung von __property, und daher sollte es mit jedem Datentyp gehen. Wenn __property nur für Wertetypen und Zeiger sein soll, dann wäre es ein Leichtes gewesen, das zur compile time zu prüfen und entsprechende Fehler anzuzeigen.
Es geht ja auch (abgesehen von obigem Bug).
DocShoe schrieb:
Natürlich gebe ich Implementationsdetails preis, aber das tue ich mit jeder Property, die Objekte zurückgibt. Selbst wenn ich Zeiger auf Objekte zurückgebe tue ich das, selbst wenn es Zeiger auf Interfaces sind.
Das ist nicht, was ich meinte. Ich bezog mich darauf, daß der Compiler etwas lax ist beim Überprüfen der Zugriffe; das wird oft mehr oder weniger unabsichtlich von Benutzern mißbraucht (z.B. kann man Properties, die für Getter und Setter eine Membervariable verwenden, auch als Referenz verwenden).
Folgender Beispielcode aus einem QC-Report, den ich einmal verfaßt hatte, dürfte das etwas näher erklären:
#include <vcl.h> #pragma hdrstop class MyClass { private: Char FChar; int FInt; AnsiString FAnsiString; WideString FWideString; void __fastcall SetChar (Char Value) { FChar = Value; } void __fastcall SetInt (int Value) { FInt = Value; } int __fastcall GetInt (void) { return FInt; } void __fastcall SetAnsiString (AnsiString Value) { FAnsiString = Value; } void __fastcall SetWideString (WideString Value) { FWideString = Value; } WideString __fastcall GetWideString (void) { return FWideString; } public: MyClass (void) : FInt (0), FChar (0) {} __property Char MyChar = { read = FChar, write = FChar }; __property int MyInt = { read = GetInt, write = SetInt }; __property AnsiString MyAnsiString = { read = FAnsiString, write = SetAnsiString }; __property WideString MyWideString = { read = GetWideString, write = SetWideString }; }; void foo (Char& i) { i = 'A'; } int main (void) { MyClass mc; // Case 1: works as expected. // Code is translated to mc.SetInt (mc.GetInt () + 2); mc.MyInt += 2; // Case 2: works, but relies on implementation detail. // Code is translated to foo (mc.FChar); foo (mc.MyChar); // Case 3: accesses private field directly although property // definition specifies a setter method. // Code is translated to mc.FAnsiString.sprintf ("test"); // mc.FAnsiString += "concat"; mc.MyAnsiString.sprintf ("test"); mc.MyAnsiString += "concat"; // Case 4: modifies temporary variable returned by GetMyUnicodeString // and therefore has no effect. // Code is translated to mc.GetWideString ().sprintf ("test"); mc.MyWideString.sprintf (L"test"); }
Wenn du tatsächlich einfach nur eine Variable öffentlich sichtbar machen willst, dann würde ich nicht ein Property verwenden, sondern sie als public deklarieren. Ein Property bringt dir zumindest in deinem Falle keinen Vorteil, da er dich auf deine Festlegung von Setter und Getter fixiert; bei Code wie
t.V.push_back (2);
hängen die Auswirkungen, wie mein Codeausschnitt erklärt, davon ab, wie du Setter und Getter definiert hast.In Delphi sind Properties in jedem Kontext außer der direkten Zuweisung konstante R-Values; so sollte das eigentlich auch in C++Builder sein. Genau dieses Thema wurde aber in o.g. Thread bereits erschöpfend diskutiert; du kannst ja mal nachlesen, sobald der Newsserver wieder verfügbar ist.
DocShoe schrieb:
In der VCL gibt es zuhauf Zeiger auf TStringList (oder deren Interfaceklasse TStrings) , die per __property zurückgegeben werden und per AddString() modifiziert werden können. Um das Dilemma in den Griff zu bekommen benutzt Borland das OnChange Ereignis, damit die umgebende Klasse Änderungen an der StringList mitbekommt. Das ist IMHO ein Workaround für dieses Problem, wenn man das wirklich sauber lösen möchte muss man wohl für jede nutzbare TStringList Methode eine Proxy Methode in der umgebenden Klasse bereitstellen, was natürlich total blödsinnig weil zu aufwändig ist. Daher sehe ich es als durchaus sinnvoll an, Referenzen auf member zurückzugeben.
Sehe ich ebenso.
DocShoe schrieb:
Ausserdem führen Objektkopien bei grossen Objekten zu Performance Hits, z.B. wenn ein deque 100.000 Elemente hat, die bei jedem getter Aufruf kopiert werden müsste.
Das sagte ich doch.
audacia schrieb:
Properties sind nur für die Rückgabe von Wertetypen gedacht (worunter auch Zeiger fallen). Fragwürdigerweise ist zwar auch std::vector<> ein solcher, und deshalb funktioniert das auch, aber die Verwendung einer Getter-Funktion führt in der Regel zur Erstellung einer Kopie, und das ist nur bei tatsächlichen Wertetypen unproblematisch.
Eine Beschränkung auf Wertetypen bedeutet für einen C++-Programmierer nichts, denn in C++ gibt es überhaupt nur Wertetypen (eine Ausnahme in C++Builder sind Delphi-Klassen, die eben nur per Zeiger übergeben werden können). Mit "tatsächlichen Wertetypen" meine ich solche, bei denen die Kopiersemantik naheliegend und gut begründet ist. Container gehören da IMHO nicht dazu.