namespace cpp

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


lernen:checklist

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.

Link zu dieser Vergleichsansicht

lernen:checklist [2014-07-13 16:14] (aktuell)
Zeile 1: Zeile 1:
 +====== Eine Checkliste für Klassen-Autoren ======
 +
 +>    Knowledge can be acquired systematically;​ wisdom cannot.
 +>>​--- ​   Andrew Koenig [Ruminations]
 +
 +===== Zielstellung =====
 +
 +Beim Klassen-Entwurf in C++ sind viele Aspekte gleichzeitig zu beachten. ​
 +Und wenn vieles gleichzeitig geschieht, kann auch vieles zugleich schief gehen [Murphy]. ​
 +Eine Checkliste kann helfen, nichts aus dem Auge zu verlieren.
 +
 +Es gibt eine Menge Bücher über C++. 
 +Die meisten (guten) wurden in Englisch geschrieben. ​
 +Und leider wurden einige der guten (bisher) nicht ins Deutsche übersetzt. ​
 +Ist der deutsche Markt zu klein? Fehlen gute Übersetzer? ​
 +Dieser Text gibt gekürzt ein Kapitel aus einem englisch geschriebenen Buch [Ruminations] wieder. ​
 +Ich hoffe, dass diese Fassung trotzdem geeignet ist, Neulingen Anregung zu geben. ​
 +Die genauer Interessierten ermuntere ich, zum Original zu greifen --- und Englisch zu lernen!
 +
 +Jeder gute Pilot benutzt eine Checkliste. ​
 +Naive Passagiere ziehen verwundert die Augenbrauen hoch: 
 +Wer weiß, wie man ein Flugzeug fliegt, braucht so ein Ding sicher nicht! ​
 +Dennoch: Sich um die Sicherheit kümmernde Piloten erklären, ​
 +wie wichtig ein Schutzmechanismus ist, 
 +um nichts Wichtiges in einem Moment der Ablenkung zu vergessen. ​
 +Auch erfahrene Berufspiloten haben schon vergessen, das Fahrwerk auszufahren, ​
 +bis sie daran erinnert wurden, von einer Warnhupe oder den die Landebahn ritzenden Propellern.
 +
 +Eine Checkliste ist keine "Du must ... tun"​-Liste. ​
 +Ihr Zweck ist, an Dinge zu erinnern, die du vergessen haben könntest, ​
 +statt dich einzuschränken. Wer einer Checkliste blind folgt, ​
 +ohne darüber nachzudenken,​ wird wahrscheinlich auch Dinge vergessen.
 +
 +Dies sind Fragen, die sich stellen, wenn du Klassen definierst. ​
 +Es gibt keine "​richtigen"​ Antworten auf diese Fragen. ​
 +Es kommt darauf an, über sie nachzudenken und sicherzustellen,​ dass, 
 +was immer du tust, das Ergebnis einer bewussten Entscheidung ist und nicht Zufall.
 +
 +===== Checkliste Klassen-Entwurf =====
 +
 +  *  Braucht deine Klasse einen Konstruktor?​
 +  *  Sind deine Daten-Attribute privat?
 +  *  Hat deine Klasse einen Konstruktor ohne Parameter?
 +  *  Initialisiert jeder Konstruktor jedes Daten-Attribut?​
 +  *  Braucht die Klasse einen Destruktor?
 +  *  Braucht die Klasse einen virtuellen Destruktor?
 +  *  Braucht die Klasse einen Kopierkonstruktor?​
 +  *  Braucht die Klasse einen Zuweisungsoperator?​
 +  *  Behandelt der Zuweisungsoperator die Selbstzuweisung richtig?
 +  *  Braucht die Klasse Vergleichsoperatoren?​
 +  *  Hast du delete[ ] zum Löschen eines Feldes geschrieben?​
 +  *  Sind die Originale von Kopierkonstruktor und Zuweisungsoperator const?
 +  *  Sollten Referenz-Argumente von Funktionen als const deklariert werden?
 +  *  Hast du die Methoden richtig als const deklariert?
 +
 +==== Braucht deine Klasse einen Konstruktor?​ ====
 +
 +Falls die Antwort "​Nein"​ ist, liest du das vielleicht zu unpassender Zeit. 
 +Einige Klassen brauchen keine Konstruktoren,​ weil sie so einfach sind, 
 +dass ihre Struktur ihre Schnittstelle ist. 
 +Wir haben jedoch meist mit Klassen zu tun, die kompliziert genug sind.
 +
 +==== Sind deine Daten-Attribute privat? ====
 +
 +Öffentliche Daten sind oft eine schlechte Idee, 
 +weil der Klassen-Schöpfer keinen Einfluss auf den Zugriff hat:
 +
 +<code cpp>
 +class Array
 +{
 +public:
 +  int length; // sehr zweifelhaft!
 +  // ...
 +};
 +</​code>​
 +
 +Flexibler einsetzbar sind Abfragemethoden:​
 +
 +<code cpp>
 +class Array
 +{
 +public:
 +  int length() const; // viel besser
 +  // ...
 +};
 +</​code>​
 +
 +Auch denkbar, aber mühseliger:​
 +
 +<code cpp>
 +class Array
 +{
 +public:
 +  const int& length; // in !jedem! Konstruktor an true_length binden
 +  // ...
 +private:
 +  int true_length;​
 +};
 +</​code>​
 +
 +Es gibt keinen "​besten"​ Stil, wie Funktionen zu schreiben sind, 
 +die auf Teile der Klasse zugreifen oder diese ändern. ​
 +Zum Beispiel zwei Funktionen für die Feldgröße: ​
 +Sollen sie Namen wie set_length() und get_length() haben? ​
 +Sollen beide length() heißen und sich durch die Anzahl der Argumente unterscheiden? ​
 +Soll die ändernde Funktion einen Wert zurückliefern oder nicht? ​
 +Wenn sie einen liefert, den neuen oder den vorherigen? ​
 +Oder soll sie etwas ganz anderes liefern, etwa einen Hinweis, ​
 +ob die Anforderung erfolgreich war? 
 +Nimm eine Stil-Konvention,​ nutze sie durchgängig und dokumentiere sie klar.
 +
 +==== Hat deine Klasse einen Konstruktor ohne Parameter? ====
 +
 +Wenn eine Klasse mindestens einen Konstruktor hat und du
 +
 +  Klasse objekt;
 +
 +ermöglichen willst, ​
 +musst du ausdrücklich noch einen Konstruktor ohne Parameter schreiben. ​
 +Sonst wird das nicht möglich sein. 
 +Dies kann absichtlich Teil des Entwurfes sein, aber vergiss nicht, ​
 +darüber nachzudenken. Auch wenn du forderst, ​
 +das alle Objekte initialisiert werden müssen, ist abzuwägen, ​
 +dass du mit diesem Beharren Felder von Objekten verbietest:
 +
 +  Klasse objekte[100];​ // brauchen Konstruktor Klasse()
 +
 +==== Initialisiert jeder Konstruktor jedes Daten-Attribut?​ ====
 +
 +Die Aufgabe des Konstruktors ist, 
 +das Objekt in einen wohldefinierten Zustand zu versetzen. ​
 +Der Zustand spiegelt sich in den Datenwerten des Objekts. ​
 +Wenn irgendein Konstruktor dabei versagt, kann das künftigen Ärger andeuten.
 +
 +==== Braucht die Klasse einen Destruktor? ====
 +
 +Nicht alle Klassen brauchen einen. Es reicht gewöhnlich nachzudenken, ​
 +ob die Klasse Ressourcen allokiert, die nicht automatisch freigegeben werden. ​
 +Jeder ''​new''​-Ausdruck in den Konstruktoren braucht gewöhnlich ​
 +einen entsprechenden ''​delete''​-Ausdruck im Destruktor.
 +
 +C++11 / C++14:
 +Nutze ''​std::​make_unique<​T>​(...)''​ bzw. 
 +''​std::​make_shared<​T>​(...)''​ zum Erzeugen und
 +''​std::​unique-ptr<​T>''​ bzw. ''​std::​shared_ptr<​T>''​
 +zum Verwalten dynamischer Objekte, um das
 +RAII-Prinzip durchzusetzen (resource akquisition is initialization).
 +Dies erspart in vielen Fällen das Schreiben eines Destruktors.
 +
 +==== Braucht die Klasse einen virtuellen Destruktor? ====
 +
 +Virtuelle Funktionen sind nur bei Vererbung sinnvoll. ​
 +Klassen, die nie als Basis dienen, brauchen keinen. ​
 +Falls jedoch jemals jemand ein dynamisches abgeleitetes Objekt ​
 +über einen Basiszeiger löscht, muss der richtige Destruktor aufgerufen werden:
 +
 +<code cpp>
 +class Base
 +{
 +public:
 +  std::string eins;
 +  virtual ~Base() {}  // virtueller Destruktor in Basisklasse
 +};
 +
 +class Derived : public Base
 +{
 +public:
 +  std::string zwei;
 +};
 +
 +int main()
 +{
 +  Base* pBase = new Derived;
 +  delete pBase; ​ // ohne virtual ~B() würde string zwei nicht freigegeben
 +  //...
 +}
 +</​code>​
 +
 +Obwohl die Basisklasse keine überhaupt keine Funktionen hat, 
 +entsteht ein Speicherleck,​ wenn die Basisklasse keinen virtuellen Destruktor hat. 
 +[Anm.: Viele Bücher über C++ erwähnen dies nicht oder erst zu weit hinten.] ​
 +Virtuelle Destruktoren sind meist leer.
 +
 +==== Braucht die Klasse einen Kopierkonstruktor?​ ====
 +
 +Oft ist die Antwort "​Nein"​. ​
 +Eine Klasse mit Destruktor (vom leeren virtuellen abgesehen) ​
 +hat diesen gewöhnlich,​ weil der Ressourcen freizugeben hat, 
 +die seine Konstruktoren allokiert haben. ​
 +Dann braucht sie vermutlich auch einen Kopierkonstruktor (und Zuweisungsoperator):​
 +
 +<code cpp>
 +class Array
 +{
 +public:
 +  Array(int anzahl, float startwert); ​    // anzahl floats mit startwert
 +  ~Array();
 +  Array(const Array& original); ​           // Kopierkonstruktor
 +  Array& operator=(const Array& original); // Zuweisungsoperator
 +  //...
 +private:
 +  float *data;
 +};
 + 
 +Array a(10, 3.14f);
 +Array b = a;        // Kopie
 +b = a;              // Zuweisung
 +</​code>​
 +
 +Ohne Kopierkonstruktor erhält eine Kopie einfach einen Zeiger auf denselben dynamischen Speicher. ​
 +Werden Original und Kopie vernichtet, wird dieser Speicher unzulässig zweimal freigegeben!
 +
 +Sollen keine Kopien erzeugt oder keine Zuweisungen ausgeführt werden dürfen, ​
 +erkläre Kopierkonstruktor bzw. Zuweisungsoperator als privat oder ''​= delete''​. ​
 +Diese brauchen auch keinen Rumpf, weil sie dadurch nie aufgerufen werden können.
 +
 +==== Braucht die Klasse einen Zuweisungsoperator?​ ====
 +
 +Braucht sie einen Kopierkonstruktor, ​
 +dann braucht sie auch einen Zuweisungsoperator,​ aus denselben Gründen
 +(Rule of Three). ​
 +Der Zuweisungsoperator sollte gewöhnlich eine Referenz auf den linken Operanden ​
 +der Zuweisung liefern und enden mit der Anweisung:
 +
 +<code cpp>
 +  return *this;
 +</​code>​
 +
 +==== Behandelt der Zuweisungsoperator die Selbstzuweisung richtig? ====
 +
 +Selbstzuweisung wird so oft falsch gemacht, ​
 +dass mehr als ein Buch über C++ es vergeigt. ​
 +Zuweisung soll einen alten Wert durch einen neuen ersetzen. ​
 +Wenn wir den alten Zielwert freigeben, ​
 +bevor wir den Quellwert kopieren und Quelle und Ziel dasselbe Objekt sind, 
 +zerstören wir auch die Quelldaten vor dem Kopieren! ​
 +Der einfachste Weg das Problem zu vermeiden ist, 
 +sich ausdrücklich davor zu schützen:
 +
 +<code cpp>
 +Array& Array::​operator=(const Array& quelle)
 +{
 +  if (this != &​quelle) ​ // Schutz vor a = a;
 +  {
 +    // alte Daten freigeben
 +    // neuen Speicher holen
 +    // Daten kopieren
 +  }
 +  return *this;
 +}
 +</​code>​
 +
 +Allerdings versagt auch dieses Verfahren, ​
 +wenn die Beschaffung des neuen Speichers fehlschlägt: ​
 +Nach der Speicherfreigabe kann das Objekt nicht mehr benutzt werden!
 +
 +Eleganter und ausnahmesicher ist das von [Sutter, p. 47, 172] 
 +vorgeschlagene Verfahren (copy assignment oder copy & swap):
 +
 +<code cpp>
 +#include <​algorithm>​
 +
 +Array& Array::​operator=(const Array& quelle)
 +{
 +  Array temp(quelle); ​        // das kann scheitern,
 +  std::​swap(data,​ temp.data); // zerstört aber *this nicht
 +  return *this; ​             ​
 +}                             // Freigabe alter Ressourcen im temp-Destruktor ​    
 +</​code>​
 +
 +Es nützt, den schon existierenden Kopierkonstruktor statt dessen Anweisungsfolge zu wiederholen. ​
 +Zudem ist eine (eher selten auftretende) Selbstzuweisung unkritisch. ​
 +Eine Variante mit Wertparameter erzeugt die temporäre Kopie schon beim Aufruf:
 +
 +<code cpp>
 +Array& Array::​operator=(Array quelle)
 +{
 +  swap(quelle);​
 +  return *this;
 +}
 +</​code>​
 +
 +Der Attribut-Tausch
 +
 +<code cpp>
 +void Array::​swap(Array&​ quelle)
 +{
 +  std::​swap(data,​ tmp.data); // für alle (!) Attribute
 +}
 +</​code>​
 +
 +kann bei Klassen mit umfangreichen dynamischen Daten als Zeigertausch ​
 +effizient implementiert werden und ist damit auch als öffentliche Methode interessant.
 +
 +==== Braucht die Klasse Vergleichsoperatoren?​ ====
 +
 +C++ hat allgemein verwendbare Bibliotheken und Containerklassen ​
 +für Datenstrukturen wie Listen, Mengen, Vektoren. ​
 +Diese Container arbeiten mit den Operationen der Objekte, welche sie enthalten. ​
 +Eine häufige Anforderung ist festzustellen, ​
 +ob zwei Werte äquivalent sind oder einer größer oder kleiner als der andere ist. 
 +Wenn die Klasse das Konzept der Gleichheit unterstützt,​ ist es sinnvoll, ​
 +''​operator==''​ und ''​operator!=''​ anzugeben. ​
 +Gibt es irgendeine Ordnungsrelation für die Werte der Klasse, ​
 +sind die anderen Operatoren (''<'',​ ''>''​ etc.) auch zu definieren, ​
 +auch wenn sie nicht direkt benutzt werden. ​
 +Geordnete Mengen von Objekten (''​std::​set<​T>'',​ ''​std::​map<​X,​U>''​) brauchen sie.
 +
 +==== Hast du delete[] zum Löschen eines Feldes geschrieben?​ ====
 +
 +Zu ''​new T''​ gehört ''​delete'',​ zu ''​new T[]''​ gehört ''​delete []''​. ​
 +Die seltsame ''​[ ]''​-Syntax kommt aus dem Streben, ​
 +Kompatibilität zu C und Effizienz zu verbinden. ​
 +C++ muss ''​new''​ mittels der C-Funktion ''​malloc()''​ implementieren. ​
 +Die C++-Implementierung kann jedoch nicht unbedingt herausfinden, ​
 +wie groß der freizugebende Speicher ist. 
 +Obwohl ''​malloc()''​ diese Größe speichern muss, 
 +kann dies an einer für C++ nicht sichtbaren Stelle geschehen. ​
 +Als Kompromiss fordert C++ von seinen Nutzern, anzugeben, ​
 +ob das freizugebende Speicher ein Feld ist.
 +
 +==== Sind die Originale von Kopierkonstruktor und Zuweisungsoperator const? ====
 +
 +Kopieren ändert die Originale nicht. Fast immer.
 +
 +==== Sollten Referenz-Argumente von Funktionen als const deklariert werden? ====
 +
 +Funktionen sollten nicht konstante Referenzen nur dann annehmen, ​
 +wenn sie sie zu verändern und dem Aufrufer geändert zu hinterlassen beabsichtigen.
 +
 +==== Hast du die Methoden richtig als const deklariert? ====
 +
 +Ändert eine Methode garantiert keine Werte des Objekts, erkläre sie als const, ​
 +damit sie bei konstanten Objekten genutzt werden kann [[const|const correctness]]:​
 +
 +<code cpp>
 +class Array
 +{
 +public:
 +  int  length() const; ​        // abfragen
 +  void length(int newlength); ​ // neu setzen
 +  // ...
 +};
 +</​code>​
 +
 +===== Nachbetrachtung =====
 +
 +Diese Liste provoziert natürlich die Frage, ​
 +warum C++ diese Dinge nicht irgendwie automatisch regelt. ​
 +Der Hauptgrund ist, dass es nicht möglich ist, 
 +dies zu tun und dabei sicher zu sein, es richtig getan zu haben. ​
 +Dies automatisch machen zu wollen, ist so ähnlich wie der Versuch, ​
 +Flugzeuge zu entwerfen, die ihr Fahrwerk automatisch unter den richtigen Umständen ausfahren. ​
 +Konstrukteure haben das versucht, mit begrenztem Erfolg. ​
 +Die "​richtigen Umstände"​ sind so schwer auf eine zuverlässige Weise definierbar, ​
 +die mechanisch umsetzbar ist. 
 +Daher muss in der Praxis der Pilot die "​Fahrwerk ausgefahren"​-Lampe immer noch prüfen. ​
 +Also bringt Automation nicht viel. Das Fahrwerk die ganze Zeit draußen zu lassen, ​
 +würde das Problem zwar auch lösen, erzeugt aber nicht akzeptable Kosten. ​
 +Das gleiche Argument gilt für den Entwurf einer Programmiersprache. ​
 +Einer Klasse einen virtuellen Destruktor mitzugeben, erzeugt Overhead. ​
 +Ist die Klasse winzig, kann dieser Zusatz merklich sein. 
 +Allen Klassen virtuelle Destruktoren mitzugeben widerspricht der Philosophie von C++, 
 +die Leute nur für das bezahlen zu lassen, was sie benutzen. ​
 +Es wäre so, wie das Fahrwerk immer ausgefahren zu lassen. ​
 +Die Alternative wäre, den Compiler irgendwie herausfinden zu lassen, ​
 +wann eine Klasse einen virtuellen Destruktor braucht und ihn dann automatisch anzulegen. ​
 +Wiederum liegt das Problem in der exakten Definition, wann das zu geschehen hat. 
 +Wäre die Definition nicht perfekt, müssten die Programmierer es nachprüfen. ​
 +Das wäre mindestens genauso viel Arbeit wie den virtuellen Destruktor selbst am richtigen Ort festzulegen.
 +
 +Mit anderen Worten: C++ ist bevorzugt etwas für Programmierer,​ die selbst denken. ​
 +C++ zu nutzen erfordert Denken, in derselben Weise wie jedes professionelle Werkzeug.
 +
 +===== Quellen =====
 +
 +  * [Murphy] Joachim Graf: Murphys Computergesetze. Markt&​Technik (1990). ISBN 3-89090-949-3.
 +  * [Ruminations] Andrew Koenig, Barbara Moo: Ruminations on C++. A decade of Programming Insight and Experience. Addison-Wesley (1997). ISBN 0-201-42339-1.
 +  * [Sutter] Herb Sutter: Exceptional C++. Addsion-Wesley (2000). ISBN 0-201-61562-2.
 +
  
lernen/checklist.txt · Zuletzt geändert: 2014-07-13 16:14 (Externe Bearbeitung)