Das Deklarieren von Objekten und Methoden als const besitzt zwei große Vorteile. Zuerst einmal beschwert sich der Compiler, wenn der Vertrag bricht. Dann teilt man damit dem Anwender des Interfaces mit, dass die Funktion ihre Argumente nicht verändert. Show
Die C++ Core Guidelines bieten fünf Regeln zu konstanten Objekten oder Methoden und konstanten Ausdrücken an. Hier sind sie:
Bevor ich mich genau mit den Regeln beschäftige, möchte ich einen Ausdruck klären, der häufig im Zusammenhang mit Konstantheit und Unveränderlichkeit genannt wird: "const correctness". Entsprechend der C++-FAQ heißt dies:
Heute geht es also um "const correctness". Con.1: By default, make objects immutableDiese Regel ist recht einfach. Du kannst einen Wert eines Built-in-Typs oder eine Instanz eines benutzerdefinierten Typs als konstant deklarieren. Das Ergebnis ist das gleiche. Wenn du versuchst, diesen zu ändern, erhältst du das, was du verdienst: einen Compile-Fehler: struct Immutable{ Die Fehlermeldung des GCC, eingebettet im Listing, ist sehr überzeugend. Con.2: By default, make member functions constMethoden einer Klasse als "const" zu erklären, besitzt doppelten Mehrwert. In diesem Fall dürfen keine nicht-konstante Methoden eines konstantes Objekt aufgerufen werden und die konstante Methode kann das zugrunde liegende Objekt nicht modifizieren. Und nochmals. Hier ist ein einfaches Beispiel, das die Fehlermeldung des GCC eingebettet enthält: struct Immutable{ Dies war nur die halbe Wahrheit. Manchmal gilt es, zwischen der logischen und physikalischen Konstantheit eines Objekts zu unterscheiden. Klingt komisch. Oder?
Die physikalische Konstantheit ist einfach zu verdauen, die logische Konstantheit hingegen nicht. Lasse mich das vorherige Programm ein wenig verändern. Nimm dazu an, dass ich das Attribut val in einer konstanten Methode verändern will: // mutable.cpp Dank des Bezeichners mutable (1) ist die Magie möglich. Das konstante Objekt kann daher die konstante Methode (2) aufrufen, die val verändet. Wozu ist mutable nützlich? Hier kommt ein netter Anwendungsfall Stelle dir vor, deine Klasse hat eine "constant read"-Funktion. Da du Instanzen der Klasse concurrent verwendest, musst du die Funktion durch einen Mutex schützen. Daher erhält die Klasse einen Mutex und du lockst diesen in der read Methode. Jetzt hast du ein Problem. Deine read-Methode kann nicht konstant sein, den in ihr wird der Mutex gelockt. Die Lösung besteht nun darin, den Mutex als mutable zu erklären. Die Klasse Immutable erhält eine einfache Umsetzung der Idee. Ohne mutable würde der Code nicht kompilieren:
Con.3: By default, pass pointers and references to constsWenn du einen Zeiger oder eine Refererenz auf ein konstantes Datum übergibst, ist die Absicht der Funktion eindeutig. Das referenzierte Objekt kann nicht verändert werden: void getCString(const char* cStr); Sind beide Deklarationen äquivalent? Nicht hundertprozentig. Im Falle der Funktion getCString kann der Zeiger ein Nullzeiger sein. Das heißt, du musst ihn immer vor seiner Verwendung prüfen: if (cStr .... Aber das ist noch nicht alles. Sowohl der Zeiger als auch das Objekt, auf das der Zeiger verweist, kann konstant sein. Hier sind die Variationen:
Zu kompliziert? Lies die Ausdrücke von rechts nach links. Immer noch zu kompliziert. Verwende eine Referenz auf const Die nächsten zwei Regeln möchte ich aus dem Blickwinkel der Concurrency betrachten. Daher fasse ich beide Regeln zusammen. Con.4: Use const to define objects with values that do not change after construction, undCon.5: Use constexpr for values that can be computed at compile timeWenn du eine Variable immutable zwischen Threads teilen willst und diese Variable ist als konstant deklariert, bist du fertig. Du kannst immutable ohne Synchronisation verwenden und erhältst die maximale Performanz aus deiner Maschine. Der Grund ist naheliegend. Die notwendige Voraussetzung für ein Data Race ist ein geteilter, veränderlicher Zustand.
Jetzt gibt es nur noch ein Problem zu lösen. Du musst die Variable in einer thread-sicheren Weise initialisieren. Mir fallen dazu vier Möglichkeiten ein.
Viele Entwickler übersehen die Variante 1, die am einfachsten umzusetzen ist. Du kannst mehr zur thread-sicheren Initialisierung einer Variable in dem Artikel "Sichere Initialisierung einer Variable" nachlesen. In der Regel Con.5 geht es um den Punkt 4. Wenn du eine Variable als konstanten Ausdruck constexpr double totallyConst = 5.5; erklärst, wird er zum Compilezeit initialisiert und ist damit thread-sicher. Das war noch nicht alles zu
Zuerst einmal, was heißt "pure" und was heißt vor allem eine "Art pure". Eine constexpr-Funktion kann potentiell zur Compilezeit ausgeführt werden. Zur Compilezeit gibt es keinen Zustand. Insbesondere bedeutet dies, dass zur Compilezeit ausgeführte constexpr-Funktionen reine Funktionen sein müssen. Reine Funktionen sind Funktionen, die immer den gleichen Wert zurückgeben, wenn sie mit den gleichen Argumenten aufgerufen werden. Reine Funktionen verhalten sich wie unendlich große Tabellen, in denen der Wert einfach nur nachgeschlagen wird. Diese Zusicherung, dass ein Ausdruck immer den gleichen Wert zurückgibt, wenn er mit den gleichen Argumenten bedient wird, nennt sich Referenzielle Transparenz. Reine Funktionen besitzen viele Vorteile:
Insbesondere der Punkt 2 macht reine Funktionen zu solch wertvollen Funktionen in Concurrent-Programmen. Die folgende Tabelle bringt die Charakteristiken von reinen Funktionen explizit auf den Punkt. Ich will es gerne noch explizit betonen. constexpr-Funktionen sind nicht per se rein. Sie sind nur rein, wenn sie zur Compilezeit ausgeführt werden. Wie geht's weiter?Das war es schon. Ich habe in diesem Artikel alle Regeln zur Konstantheit und Unveränderlichkeit der C++ Core Guidelines vorgestellt. In nächsten Artikel schreibe ich über die Zukunft von C++: Templates und generische Programmierung. |