Intro
Dies ist der zweite Artikel meiner kleinen Mini-Serie über vielleicht nicht so bekannte C++ Features / Pattern / Programmiertechniken für welche ich praktische Anwendungsbeispiele während der Arbeiten an meinen Projekten gefunden habe.
Dieses mal geht es um const_cast<T&>
.
Ein const_cast gilt als böse
Es ist mehr oder weniger unstrittig, dass die Verwendung von const_cast<>
– so wie ein goto
– als böse und schlechte Programmiertechnik in fast allen Fällen eingestuft wird.
Ich habe dazu ein kurzes Beispiel auf godbolt erstellt, welches die schlechte Verwendung eines const_cast
veranschaulicht. Hier ist der Link: Böses Beispiel auf Godbolt.
Dort wird ein const Member eines const Objektes von Außen ohne jeden Grund verändert.
Der Grund, warum dieses als schlechte Technik / schlechtes Design angesehen wird, ist einfach erklärt. Jeder Benutzer der Klasse Foo erwartet, dass der String Member während der gesamten Lebensdauer des Objektes konstant bleibt. Jede unerwartete Veränderung kann im schlimmsten Fall zu „undefined behavior“ führen. Z.B. weil jemand die Länge des Strings gespeichert hat und jetzt nach der Veränderung des Stringwertes (und der Länge!) einen „out of range“ Zugriff erzeugt. Oder, weil jemand sich den char Pointer gemerkt hat und dieser jetzt nach der Veränderung ungültig ist (Reallokation). Oder, oder, oder, …
Das Design der Klasse Foo trägt in sich, dass eine neue Klasseninstanz erzeugt werden soll, wenn ein anderer Wert im String gebraucht wird. Aber hier wird das Design komplett ignoriert und mit Füßen getreten, wenn der const_cast<>
benutzt wird. Deshalb mach so etwas bitte niemals in deinem Code.
Kann const_cast auch nicht böse sein?
Also gibt es denn dann überhaupt einen guten Anwendungsfall?
Ja, den gibt es. const_cast<>
kann ohne jede Gefahr und Schaden verwendet werden um „duplicate code“ zu vermeiden.
Einen wunderbaren Anwendungsfall habe ich gefunden, als ich die Collection
Klasse der TeaScript C++ Library implementierte.
Die Collection Klasse ist eine Container Klasse mit stabiler Speicherreihenfolge der Elemente, welche das LIFO Verhalten implementiert und sowohl index-basierten Zugriff anbietet, als auch (optional) per Key. Die Komplexität ist vergleichbar mit std::vector
, allerdings kommt noch extra Komplexität hinzu für den Fall, dass Keys eingesetzt werden und andere Element außer dem Letztem entfernt werden, da der Zugriff per Key gepflegt werden muss.
Den Sourcecode kann man hier ansehen: Collection.hpp
Die Verwendung eines guten const_cast sieht dann so aus:
// code from https://github.com/Florian-Thake/TeaScript-Cpp-Library/blob/main/include/teascript/Collection.hpp
// [...]
class Collection
{
public:
// [...]
ValueType const &GetValueByKey( KeyType const &rKey ) const
{
auto it = mLookup.find( rKey );
if( it != mLookup.end() ) {
assert( ContainsIdx( it->second ) );
return GetValueByIdx_Unchecked( it->second );
}
throw exception::out_of_range( "Collection: Invalid key! Key not found!" );
}
ValueType & GetValueByKey( KeyType const &rKey )
{
// one of the rare unevil const_cast:
// first make this const to can re-use the const code,
// then remove the const again from result.
return const_cast<ValueType &>(const_cast<Collection const &>(*this).GetValueByKey( rKey ));
}
// [...]
};
Erklärung
In diesem Beispiel gibt es 2 Getter (via Überlandung) um den Wert als Referenz zurück zu geben. Der eine ist für den Const-Fall, wenn die Klasseninstanz in einem Const-Kontext aufgerufen wird oder der Rückgabewert als const Referenz verwendet wird. Der andere ist nur einsetzbar in Nicht-Const-Kontexten, wenn der Wert als mutable Referenz verwendet wird.
Aber um den Rückgabewert zu erhalten, muss erst etwas Code ausgeführt werden. Muss dieser hier jetzt etwa 2-mal geschrieben werden?
Nein, mit const_cast<>
in der Nicht-Const(!) Überladung kann der „duplicate code“ vollständig verhindert werden.
Die Nicht-Const Überladung kann nur in Nicht-Const-Kontexten angewendet werden (dies impliziert ebenfalls ein nicht konstantes Objekt).
Der Trick ist dann, dass zuerst das Objekt (der this Pointer) zu einem Const-Objekt gecastet wird, um einen Const-Context zu erzeugen, um dann die Const Überlandung aufzurufen. Der konstante Rückgabewert kann dann sicher (ohne Gefahr, ohne Schaden) in eine mutable Referenz mit const_cast<>
gecastet werden, da er ja in Wirklichkeit schon mutable ist, da wir uns im Nicht-Const-Kontext befinden. Wir haben es vorher nur künstlich const gemacht, was jetzt einfach wieder entfernt werden kann.
Damit kann der „duplicate code“ vollständig vermieden werden. 🙂
Fazit
Persönlich denke ich, dass dies ein sehr gutes Beispiel ist um „duplicate code“ zu vermeiden bei const und Nicht-Const Überladungen.
Was denkst du über diese Technik und/oder dem konkreten Anwendungsbeispiel?
Ich hoffe, der Artikel hat dir gefallen und ich ‘sehe’ dich im dritten Teil wieder. Danke fürs Lesen! 🙂