Was lange währt, wird endlich wahr

Modernes C++11 (Teil 4)
Kommentare

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

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.

Thomas Trotzki ist ALM Consultant bei der artiso AG nahe Ulm und Microsoft-C++-Profi der ersten Stunde. Mit Microsoft C++ und den MFC beschäftigt er sich intensiv seit den ersten Betaversionen zu Microsoft C/C++ 7.0, also bereits vor der Geburtsstunde von Visual C++ und Visual Studio. Technologisch ist er neben C++ und den MFC auch mit COM/DCOM und der gesamten „Managed Welt“ vertraut und hat umfangreiche Expertise im Application Lifecycle Management.
Christian Binder arbeitet als ALM Architect in der Developer Platform & Strategy Group bei Microsoft Deutschland. Er arbeitet seit 1999 bei Microsoft, u. a. als Escalation Engineer, dann Platform Strategy Manager und kann so auf umfangreiche Erfahrungen im Bereich Application Development zurückgreifen. Auch war er im Product Development von Microsoft in Redmond tätig, was ihn 2005 zum Thema Application Lifcycle Management gebracht hat.
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -