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; | ||
+ | >> | ||
+ | |||
+ | ===== 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" | ||
+ | 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, | ||
+ | |||
+ | Dies sind Fragen, die sich stellen, wenn du Klassen definierst. | ||
+ | Es gibt keine " | ||
+ | Es kommt darauf an, über sie nachzudenken und sicherzustellen, | ||
+ | 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 " | ||
+ | Einige Klassen brauchen keine Konstruktoren, | ||
+ | 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! | ||
+ | // ... | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Flexibler einsetzbar sind Abfragemethoden: | ||
+ | |||
+ | <code cpp> | ||
+ | class Array | ||
+ | { | ||
+ | public: | ||
+ | int length() const; // viel besser | ||
+ | // ... | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Auch denkbar, aber mühseliger: | ||
+ | |||
+ | <code cpp> | ||
+ | class Array | ||
+ | { | ||
+ | public: | ||
+ | int const& length; // in !jedem! Konstruktor an true_length binden | ||
+ | // ... | ||
+ | private: | ||
+ | int true_length; | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | Es gibt keinen " | ||
+ | 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, | ||
+ | |||
+ | ==== 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]; | ||
+ | |||
+ | ==== 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 '' | ||
+ | einen entsprechenden '' | ||
+ | |||
+ | C++11 / C++14: | ||
+ | Nutze '' | ||
+ | '' | ||
+ | '' | ||
+ | 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; | ||
+ | //... | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Obwohl die Basisklasse keine überhaupt keine Funktionen hat, | ||
+ | entsteht ein Speicherleck, | ||
+ | [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 " | ||
+ | Eine Klasse mit Destruktor (vom leeren virtuellen abgesehen) | ||
+ | hat diesen gewöhnlich, | ||
+ | die seine Konstruktoren allokiert haben. | ||
+ | Dann braucht sie vermutlich auch einen Kopierkonstruktor (und Zuweisungsoperator): | ||
+ | |||
+ | <code cpp> | ||
+ | class Array | ||
+ | { | ||
+ | public: | ||
+ | Array(int anzahl, float startwert); | ||
+ | ~Array(); | ||
+ | Array(Array const& original); | ||
+ | Array& operator=(Array const& original); // Zuweisungsoperator | ||
+ | //... | ||
+ | private: | ||
+ | float *data; | ||
+ | }; | ||
+ | |||
+ | Array a(10, 3.14f); | ||
+ | Array b = a; // Kopie | ||
+ | b = a; // Zuweisung | ||
+ | </ | ||
+ | |||
+ | 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 '' | ||
+ | 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, | ||
+ | (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; | ||
+ | </ | ||
+ | |||
+ | ==== 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:: | ||
+ | { | ||
+ | if (this != & | ||
+ | { | ||
+ | // alte Daten freigeben | ||
+ | // neuen Speicher holen | ||
+ | // Daten kopieren | ||
+ | } | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 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 < | ||
+ | |||
+ | Array& Array:: | ||
+ | { | ||
+ | Array temp(quelle); | ||
+ | std:: | ||
+ | return *this; | ||
+ | } // Freigabe alter Ressourcen im temp-Destruktor | ||
+ | </ | ||
+ | |||
+ | 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:: | ||
+ | { | ||
+ | swap(quelle); | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Der Attribut-Tausch | ||
+ | |||
+ | <code cpp> | ||
+ | void Array:: | ||
+ | { | ||
+ | std:: | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 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, | ||
+ | '' | ||
+ | Gibt es irgendeine Ordnungsrelation für die Werte der Klasse, | ||
+ | sind die anderen Operatoren (''<'', | ||
+ | auch wenn sie nicht direkt benutzt werden. | ||
+ | Geordnete Mengen von Objekten ('' | ||
+ | |||
+ | ==== Hast du delete[] zum Löschen eines Feldes geschrieben? | ||
+ | |||
+ | Zu '' | ||
+ | Die seltsame '' | ||
+ | Kompatibilität zu C und Effizienz zu verbinden. | ||
+ | C++ muss '' | ||
+ | Die C++-Implementierung kann jedoch nicht unbedingt herausfinden, | ||
+ | wie groß der freizugebende Speicher ist. | ||
+ | Obwohl '' | ||
+ | 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; | ||
+ | void length(int newlength); | ||
+ | // ... | ||
+ | }; | ||
+ | </ | ||
+ | |||
+ | ===== 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 " | ||
+ | die mechanisch umsetzbar ist. | ||
+ | Daher muss in der Praxis der Pilot die " | ||
+ | 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, | ||
+ | C++ zu nutzen erfordert Denken, in derselben Weise wie jedes professionelle Werkzeug. | ||
+ | |||
+ | ===== Quellen ===== | ||
+ | |||
+ | * [Murphy] Joachim Graf: Murphys Computergesetze. Markt& | ||
+ | * [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