Was lange währt, wird endlich wahr

Modernes C++11 (Teil 2)
Kommentare

Windows Developer

Der Artikel „Was lange währt, wird endlich wahr“ von Thomas Trotzki und Christian Binder ist erstmalig erschienen im Windows Developer 9.2012
Smart Pointer
Seit der Einführung von

Windows Developer

Der Artikel „Was lange währt, wird endlich wahr“ von Thomas Trotzki und Christian Binder ist erstmalig erschienen im Windows Developer 9.2012

Smart Pointer

Seit der Einführung von Templates lassen sich Smart Pointer sehr einfach realisieren. Das Template auto_ptr wird von den meisten C++-Compilern seit Jahren unterstützt. Bibliotheken wie boost haben dieses Pattern in den letzten Jahren nennenswert erweitert. Die entsprechenden Smart-Pointer-Templates haben in C++ STL nun eine Normierung erfahren. unique_ptr löst dabei den bereits angesprochenen auto_ptr (deprecated) ab und stellt den einfachsten Smart Pointer dar:

{
  unique_ptr p = new sometype;
  p->func();
} // dtor of p deletes "new sometype"  

Sobald jedoch mehrere Bezüge auf ein Objekt verwaltet werden müssen, genügt das einfache Modell von unique_ptr nicht mehr. Das Objekt auf dem Heap darf erst freigeben werden, nachdem der letzte Bezug seinen Gültigkeitsbereich verlassen hat. Hier hilft shared_ptr (Listing 1).

Listing 1

{
    shared_ptr sp1;

    {
      shared_ptr< sometype > sp2;
      sp2 = make_shared< sometype>();
      sp1 = sp2;
    } // sp2 goes out of scope
} // sp1 goes out of scope  

shared_ptr arbeitet über einen Referenzzähler, der bei Zuweisungen inkrementiert und durch den Destruktor dekrementiert wird. make_shared sorgt dafür, dass der Referenzzähler beim Anlegen des Objektes sicher arbeitet und kapselt den Aufruf von new. Ein Problem bei Reference Counting stellen zyklische Referenzen dar. Zwei sich gegenseitig referenzierende Objekte halten den jeweiligen Zähler auf 1, die Objekte werden somit nicht mehr freigeben. Auch dies ist berücksichtigt, die STL bietet hierfür das Template weak_ptr an. Neben den eigentlichen Smart Pointern sorgt die STL mit einer ganzen Menge an überladenen Operatoren dafür, dass der Umgang mit Smart Pointern einfach und recht fehlerresistent ist. Richtig angewendet, wird damit der Operator delete in eigenen Anwendungen nahezu überflüssig. Es genügt, sich einmalig bei der Vereinbarung des Zeigers Gedanken über den Lebenszyklus des dahinterstehenden Objektes zu machen und den richtigen Smart Pointer einzusetzen, den Rest regelt dieser selbst. Wer weiterhin hartnäckig auf den Einsatz von delete besteht, kann dies natürlich tun. Er muss sich dann aber bei Verlassen des Gültigkeitsbereiches eines Zeigers Gedanken machen, was mit dem dahinter stehenden Objekt geschehen soll, und das wird auf deutlich mehr Stellen im Code hinauslaufen.

Lambdas

Callbacks sind ein uraltes Konzept und stellen ein wesentliches Leistungsmerkmal dar, das moderne Softwarearchitekturen erst möglich macht. Layer-basierte Software setzt voraus, dass die Abhängigkeit zwischen den Schichten nur in eine Richtung geht: von oben nach unten. Komponentenbasierte oder serviceorientierte Modelle nutzen andere Begriffe, meinen aber das gleiche: Eine Anwendung nutzt eine Komponente oder einen Service, nicht umgekehrt. Ohne einen Callback wäre damit auch die Richtung der Funktionsaufrufe eine Einbahnstraße, damit sind heutige Anwendungen nicht zu realisieren. C und C++ bieten zur Realisierung eines Callback klassische herkömmliche Funktionszeiger, Zeiger auf Elementfunktionen, an. In C++ steht natürlich auch die Nutzung von Schnittstellen als Option offen. Gemeinsam haben alle Ansätze, dass die Signatur der Funktion von der Komponente definiert und bereits genutzt werden kann, die Implementierung aber erst später durch die Anwendung oder eine höhere Schicht nachgereicht wird. Nur auf diese Weise können Komponenten kompiliert werden, ohne die konkreten Nutzer benennen zu müssen, das Henne-Ei-Problem ist dadurch gelöst.

Seitdem dieses Modell verinnerlicht ist, haben immer mehr Bibliotheken ausführlich davon Gebrauch gemacht. Die Folge ist, dass viele Anwendungen zwischenzeitlich aus mehr Callbacks bestehen als aus eigentlichen Funktionen. Und da alle bisherigen Ansätze Funktionen zur Implementierung des Callback voraussetzen, wird natürlich auch die Unterscheidung zwischen Funktion und Callback – und damit die Aufrufrichtung – recht schwierig. Ansätze mit OnIrgendwas kennt jeder, doch eine Namenskonvention ist eben nur eine Namenskonvention. Lambdas liefern hier eine Lösung, denn sie erlauben die Definition des Callback an jeder beliebigen Stelle im Code, insbesondere eben auch innerhalb einer anderen Funktion. Und damit rutscht die Implementierung vom Global oder Class Scope in den Function Scope. Sie wird ganz exakt zugeordnet und weggekapselt. Ein Lambda kann somit auch als lokale Funktion bezeichnet werden, ist syntaktisch jedoch noch deutlich mächtiger. Mit einem Lambda wird eine lokale Funktion vereinbart. Abbildung 1 zeigt die Bestandteile eines Lambdas:

Abb. 1: Lambda
Abb. 1: Lambda
  1. Die Parameterliste vereinbart die Lambda-Parameter. Diese werden später beim Aufruf wie bei herkömmlichen Funktionen angegeben.
  2. Der Rückgabetyp steht an einer anderen Stelle, ansonsten gelten aber die altbekannten Regeln. Ausnahme: Wird er nicht explizit angegeben, so richtet er sich nach dem return im Rumpf.
  3. Der Funktionsruf wird wieder identisch wie bei einer Funktion vereinbart.
  4. throw gibt an, ob Exceptions ausgelöst werden, auch hier gelten die Regeln wie bei Funktionen.
  5. Über die Capture Clause wird geregelt, wie mit lokalen Variablen der umschließenden Funktion umgegangen werden soll.
  6. mutable erlaubt eine feinere Regulierung des Verhaltens der in der Capture Clause angegebenen Variablen.

Die Capture Clause ist es, die das Besondere am Lambda darstellt und einer genaueren Betrachtung bedarf. Einige Beispiele werden in Listing 2 dargestellt. Die beiden Lambdas lambda1 und lambda2 zeigen jeweils die Vereinbarung und die Nutzung, sprich den Aufruf, einmal ohne und einmal mit Parameter. Dabei wird deutlich, dass das Lambda selbst einen Wert darstellt, der einer Variablen zugewiesen werden kann. Dank auto machen wir uns in der Regel keine weiteren Gedanken mehr über den exakten Typ der Variablen, es steckt tatsächlich aber ein Template dahinter, das die unterschiedlichen Ausprägungen möglicher Lambdas typsicher macht – man denke alleine an die Anzahl der Lambda-Parameter.

Listing 2: Beispiele zu Lambdas

void DemoLambda()
{
  auto lambda1 = [] ()   
  { 
    cout << "lambda1" << endl; 
  };
  lambda1();

  string strMessage = "hello again";
  // try 'string s', 'string &s'
  auto lambda2 = [] (string s)
  {
    cout << "lambda2: " << s << endl;
  };
  lambda2(strMessage);
  strMessage = "good bye my dear";
  lambda2(strMessage);

  strMessage = "hello again";
  // try [], [=], [&], [strMessage], [&strMessage]
  auto lambda3 = [&strMessage] ()
  {
    cout << "lambda3: " << strMessage << endl;
  };
  lambda3();
  strMessage = "good bye my dear";
  lambda3();

  int a=1;
  // try '[=]', '[=] mutable', [&]
  auto lambda4 = [=] () mutable
  {
    cout << "lambda4: " << a++ << endl;
  };
  lambda4();
  a = 56;
  lambda4();
  cout << "a: " << a << endl;

  auto lambda5 = [] (int param) -> int
  {
    cout << "lambda5: " << param << endl;
    return 2*param;
  };
  a = lambda5(a);
  a = lambda5(a);
  cout << "a: " << a << endl;
}  
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -