namespace cpp {}

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


lernen:checklist
no way to compare when less than two revisions

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.


lernen:checklist [2020-07-27 09:30] (aktuell) – angelegt - Externe Bearbeitung 127.0.0.1
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:
 +  int const& 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(Array const& original);            // Kopierkonstruktor
 +  Array& operator=(Array const& 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=(Array const& 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=(Array const& 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: 2020-07-27 09:30 von 127.0.0.1

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki