copy Sematik
rvalue references erscheinen auf den ersten Eindruck recht „theoretisch“. Was ist so toll daran? Dazu folgender Codeauszug:
mystring f() { mystring result("we will return this"); return result; }
Was ist hier das Problem? Beim dem einen oder anderen C++-Programmierer läuten hier bereits die Alarmglocken, denn er hat dieses Problem selbst schon einmal durchleben müssen. Hier noch eine weitere, analoge Situation:
void f(mystring s) { }
Problematisch wird die Sache, wenn unsere Klasse mystring den Inhalt dynamisch auf dem Heap ablegt, in einem herkömmlichen Zeiger speichert und im Destruktor den Speicher bereinigt:
class mystring { // ... skipped char *content; ~mystring() { delete content; } };
C++ arbeitet standardmäßig mit Value Types, auch ein char * ist einer. Und Value Types werden standardmäßig binär kopiert. Somit entstehen in beiden dargestellten By-Value-Szenarien zwei Zeiger auf den gleichen Heap-Speicher. Allgemeiner ausgedrückt, hängt dieses Problem ständig wie ein Damoklesschwert über uns C++-Programmierern. Sobald eine Klasse einen Destruktor besitzt, sollten wir uns zumindest Gedanken machen. Die Lösung des Problems ist seit Langem in C++ definiert. Es muss ein Copy-Konstruktor definiert oder der Standard-Copy-Konstruktor zumindest deaktiviert werden:
class mystring { // ... skipped mystring(const mystring& init); };
Für das Deaktivieren genügt es, den Copy-Konstruktor private zu vereinbaren, ansonsten bedarf es natürlich einer Implementierung, die den hinter init stehenden Inhalt in das neue Objekt kopiert. Analog gilt das Ganze übrigens auch für die Zuweisung, also operator= (Listing 4).Damit ist das Problem an und für sich gelöst, jedoch ist dies nicht immer unbedingt sonderlich effizient. Hinsichtlich Heap entsteht folgende Aufrufsequenz:
Listing 4
class mystring { // ... skipped char *content; ~mystring() { delete content; } mystring(const char *init); mystring(const mystring& init); mystring& opertor=(const mystring& rhs); }; mystring f() { my string result("content from f"); return result; } void g() { mystring s1("content from g"); s1 = f(); }
- s1.content = new – „content from g“ (1)
- result.content = new – „content from f“ (2)
- rhs.content = new – „content from f“ (3)
- delete result.content – „content from f“ (2)
- delete s1.content – „content from g“ (1)
- s1.content = new – „content from f“ (4)
- delete rhs.content – „content from f“ (3)
- delete s1.content – „content from f“ (4)
Die mit (2) und (3) gekennzeichneten Aufrufe sind unnötig, denn eigentlich genügt es, einmalig den Speicher für „content from g“ freizugeben und den neuen Speicher für „content from f“ anzulegen. Die Copy-Semantik führt zu einem Overhead mit Faktor 3.
move-Semantik
Die Lösung des Copy-Problems kommt mit der C++11 move-Semantik. Diese wiederum macht auch anschaulich, welchen Nutzen die Unterscheidung zwischen lvalue und rvalue reference beim Überladen von Funktionen bringen kann. Tatsächlich ist es nämlich so, dass gerade für rvalues das Erzeugen einer Kopie unnötig ist, hier genügt es den Inhalt zu verschieben (Listing 5). Besonders ist hier noch die Verwendung von move(init.content) zu erwähnen. Definiert der an move übergebene Parameter selbst einen move-Konstruktor, so wird dieser aufgerufen, ansonsten wird eine bitweise Kopie erstellt.
Listing 5
class mystring { // ... skipped char *content; ~mystring() { delete content; } mystring(const char *init); mystring(const mystring & init); mystring(mystring && init); mystring& opertor=(const mystring& rhs); mystring& opertor=(mystring&& rhs); }; mystring::mystring(string&&init) : data(move(init.content) { init.content = nullptr; }
Neuerungen bei der Vererbung
Auch bei der Vererbung gibt es einige kleine Änderungen, die C++ ein Stück besser machen. Bekannt sind die entsprechenden Konzepte von Java und C#. Mit dem neuen Schlüsselwort override beim Überschreiben von virtuellen Funktionen in einer abgeleiteten Klasse wird der Compiler angewiesen, abzuprüfen, dass eine entsprechende virtuelle Funktion in einer Basisklasse auch tatsächlich existiert und ein Fehlen mit einem Compilerfehler zu quittieren. Damit wird ein versehentliches Überladen anstelle des gewünschten Überschreibens abgefangen. Besonders hilfreich ist override damit nicht nur beim erstmaligen Implementieren der abgeleiteten Klasse, sondern auch beim Pflegen der Signaturen der virtuellen Funktionen in der Basisklasse (Listing 6). Ebenso erlaubt es C++11 ein Überschreiben einer virtuellen Funktion in einer weiteren Ableitung zu unterbinden. Hierfür wird das Schlüsselwort final analog zu override verwendet:
Listing 6
class base { public: virtual void f(int); }; class derived : public base { public: virtual void f(int) override; };
class derived : public base { public: virtual void f(int) final; };
Und zu guter Letzt kann auch eine weitere Ableitung komplett unterbunden werden, hierfür wird final ans Ende einer Klassenvereinbarung gestellt:
class derived final : public base { };
Fazit
Mit C++11 wird C++ erneut ein Stück moderner und beinhaltet nun alle Leistungsmerkmale, die bislang bei einem Vergleich mit Java und C# eventuell schmerzlich vermisst wurden. Durch sinnvollen Einsatz der STL werden unbeabsichtigten Fehler deutlich unwahrscheinlicher und in C++ erstellte Anwendungen robuster. Auf einen Garbage Collector verzichten zu müssen, ist keine Schande, im Gegenteil: In keiner anderen Sprache kann so gezielt jederzeit entschieden werden, ob gerade Abstraktion oder Laufzeit eine höhere Bedeutung haben soll wie in C++. Damit ist und bleibt C++ die mächtigste Programmiersprache schlechthin. Modernes C++11 hat mit auto, lamdba, sharedPtr und Co. im Kontext von Produktivität einen deutlichen Schritt nach vorne gemacht. Es lohnt sich, diese Mechanismen zu nutzen. Auf die nächsten 30 Jahre!
Doch es wird noch weiter gehen. Auf der ADC ++ Anfang Mai in Ohlstadt haben Michael Wong (IBM, Representative to the C++ commitee ), Steve Teixeira und Boris Jabes (Microsoft-C++-Team ) Einblick in die aktuellen Themen des C++ Commitee gegeben. Als Nächstes stehen in dieser Artikelserie Themen wie Multitasking und weitere große Bibliotheken zur Normierung an.