krone2023-01
Schulung Fa. Krone, Spelle
6. bis 8. Februar 2023
https://tbfe.de/krone2023-01
mailto:mw@tbfe.de
- Montag 6. Februar
- Begrüßung und Kurzvorstellung
- Abstimmung der Agenda
- Überblick zum Thema Datentypen
- eingebaute Datentypen
- arithmetische Datentypen
- Klassen als Datentypen
- Typ-Reinheit: Klassen vs. Typ-Aliase
- Zeiger vs. Referenzen (DT-18ff)
- Gemeinsamkeiten und Unterschiede
- Kombination mit `const` (DT-16)
- Übergang zwischen Zeigern und Referenzen
- "Best Practices" und "Pitfalls"
- Smart-Pointer
- Was versteht man unter "RAII"?
- Direkte Verwendung von `new` und `delete`
- Zeiger als "(alleinige) Eigentümer" von Heap-Speicherplatz
- Zeiger als "Mit-Eigentümer von Heap-Speicherplatz
- Zeiger als "Beobachter" von "Heap-Speicherplatz"
- Fortgeschrittener Umgang mit "Smart-Pointern"
- Flexibilität durch "Custom Deleter"
- CRTP und `std::shared_from_this`
- Best Practices und Pitfalls
- Optionale Übung(en):
- Vereinfachungen durch `std::unique_ptr`
- Mehr Robustheit durch `std::shared_ptr`
- Pragmatisches Refactoring mit `std::weak_pointer`
- Dienstag 7. Februar
- Überblick Thema Operator-Überladung (OP-14ff)
- Kurzdemo zu den beiden Hauptformen
- Implementierung durch Freie Funktionen
- Implementierung durch Member-Funktionen
- Anwendung auf eine Zähler-Klasse
- Übung zu initialen Design-Aspekten
- "Reuse"-Aspekte (Don't Repeat Yourself)
- Fortsetzung der Übung
- Pro und Contra klassenspezifische Typ-Umwandlungen
- `explicit` für Konstrukoren (SP-9)
- Type-Cast Operatoren (SP-12)
- `explicit operator bool`
- Modularität und Variabilität durch Basisklassen
- Erweiterte Typkontrollen zur Compilezeit
- "Hausgemacht" vs. `boost::units`
- Optionale Übung "U/I/R"
- Mittwoch 8. Februar
- Schwerpunkt: OO-Design und Polymorphie (VER-3ff)
- Vererbung vs. Komposition
- Bedeutung der Substitubilität (LSP)
- Gedanken-Übung
- "Rechteck als Basisklasse für Quadrat"
- "Parkplatz" vs. "U-Boot-Hafen"
- Praxis-Beispiel "Zähler-Klasse" (Demo+Übung):
- Ausgangspunkt "0..1" Assoziation
- Vererbung mit virtueller `count`-Member-Funktion
- Pro- und Contra virtuelle `reset`-Funktion?
- Alternatives Design gemäß "NVI"-Idiom
- Optionale Übung (ggf. auch nur Demo)
- Vermeiden typ-abhängiger Verzweigungen
- Klassisches "Template Method Pattern"
- Pro- und Contras
- Optionale Übung (ggf. auch nur als Demo)
- Schwerpunkt: OO-Design und Polymorphie (VER-3ff)
- Optional (mein Vorschlag, abhängig von Zeit und Interesse)
- Flexibilität durch Templates statt Vererbung
- Demo: Anwendung auf einige der Beispiele
Kleinere Demos Montag
- https://godbolt.org/z/bsTq1KhEc
Anzahl der Bits pro `char` sowie der Größe diverser Datentypen gemessen mit `sizeof` - https://godbolt.org/z/bMzznfzbM
Problematik des "Wegnehmens von `const`" (inkonsistenter Inhalt, Programmabbruch ...) - https://godbolt.org/z/b1osY4YPj
Vergleich einer "swap_int"-Funktion realisiert mit Zeigern und mit Referenzen - https://godbolt.org/z/7rof8YEq5
Mögliches Entstehen einer "undefinierten Referenz" durch Konvertierung eines Zeigers - https://godbolt.org/z/64r7ffxfn
Kurzdemo zu `decltype` und der Ausgabe von Typ(-namen) mit dem Hilfs-Macro `PT`# - https://godbolt.org/z/7GGsnnv94
Kurzdemo zu `std::shared_ptr` (Erzeugen, Kopieren, `use_count` ...)
Weitere Links Montag
- Eigene Infografiken (ergänzend zur Schulungsunterlage):
Aufgabe Montag Nachmittag
- https://godbolt.org/z/4jhn1hYEh <---- Startvorgabe
Klasse `NamedCounter` (wird Dienstag weiter ausgebaut, u.a. zu einem kaskadierbaren Zähler)- Member-Daten (private) "Wert" (`valiue_`) und "Name" (`name_`)
- Member Funktionen "Getter/Setter" für Wert, nur "Getter" für Name
- Konstruktor (erlaubt die Vergabe eines beliebigen Namens
- 1. Problem: `name_` ist ein Zeiger und kann auf eine Adresse gesetzt werden, die bereits für andere Zwecke verwendet wird, während das erzeugte `NamedCounter`-Objekt weiter existiert
- https://godbolt.org/z/1rPoj3j6s <---- Lösung für 1. Problem
Übergebener Name wird in Heap-Speicher umkopiert- Lösungsvariante mit Speicheranforderung in der MI-Liste und anschließendem Kopieren im Konstruktor-Block (Nachteil: `name_` muss jetzt auf modifizierbare Zeichen Zeigen (aus ` char const*` wird `char*`)
- https://godbolt.org/z/5dqWG3fb5 <---- alternative Lösung für 1. Problem
- Speicheranforderung und Kopieren werden zu einer Hilfsfunktion `strdup` zusammengefasst. Dort wird auf eine lokale Hifsvariable vom Typ `char *` kopiert, diese wird dann aber an die Member-Variable vom Typ ´ char consr*` zugewiesen.
- https://godbolt.org/z/fedazdra8 <---- alternative Lösung für 1. Problem
- Speicheranforderung zusammengefasst zu einem komplexen Ausdruck. Nachteil schlecht zu analysieren, evtl. Fehleranfällig (Problem der Aufrundung des angeforderten Speicherplatzes auf eine größere Zahl. welche die Entdeckung des Problems auf spezielle Fälle beschränkt
- 2. Problem (alle Lösungsvarianten): Es wird immer wieder Heap-Speicherplatz neu angefordert aber nie freigegeben.
- https://godbolt.org/z/TK4TddPh9 <---- Lösung für 2. Problem
- Einführung eines Destruktors (` ~NamedValue`), der die Freigabe mittels `delete[]` vornimmt. Wichtig: Anforderung (Array-Form `new[]`) und Freigabe (Array-Form `delete[]` müssen zueinander passen - sonst UB (= undefiniertes Verhalten)
- 3. Problem: Wenn eine Klasse nicht selbst beschreibt, wie ein neues Objekt durch Kopieren eines vorhandenen Objekts erstellt wird, nimmt der Compiler an, dass einfach alle Member-Daten elementweise kopiert werden sollen.
- bei Rückgabe eines ` NamedValue` aus `makeCounter` wird der darin enthaltene Heap-Zeiger kopiert;
- in zwei Objekten existiert nun ein Zeiger auf den selben Heap-Speicher
- sobald das erste dieser Objekte verschwindet wird, wird dem verbleibenden der Speicherplatz für `name_` entzogen!
- https://godbolt.org/z/55ExEe6ro <---- Lösung für 3. Problem
- Dem Compiler durch eine spezielle Syntax (eingeführt in C++11) angezeigt, dass ein Kopier-Konstruktor NICHT automatisch bereitgestellt werden soll.
- 4. Problem: Es könne keinen Hilfsfunktionen mehr verwendet werrde, um einen `NamedCounter` zu erzeugen ("Factory-Funktionen")
- https://godbolt.org/z/8c4K4fnqh <---- Lösung für 4. Problem
- Da der Return-Wert einer Funkion ein "expiring value" (x-value), wird der Compiler zur Rückgabe eines `NamedCounter` dessen "Move-Konstruktor" in betracht ziehen – wenn ein solcher definiert ist.
- Dieser kann einfach den Zeiger auf Heap-Speicherplatz kopieren, wenn er diesen anschließend im "expiring" Pbjekt auf `nullptr` setzt ...
- womit dort keine Freigabe mehr erfolgt.
- 5. Problem: Der Code erledigt mittlerweile "zu Fuß", was ein `std::unique_ptr` genauso oder teilweise solar besser kann.
- der `std::unique_ptr` verhindert neben dem Kopieren eines Heap-Zeigers per Konstruktor auch das Kopieren mittels Zuweisung;
- beixdes hätte zur Freigabe dieses Speicherplatzes geführt sobald eines der beteiligten Objekte sein Leben beenden und damit vom länger lebenden Objekt noch verwendeten Speicherplatz zu frühzeitig wiederverwendet.
- https://godbolt.org/z/MxTYf3Yvj <---- Lösung für 5. Problem
- die Member-Variable `name_` wird als `std::unique_ptr` definiert;
- ihre Initialisierung durch die Hilfsfunktion `strdup` wird beibehalten;
- zu beachten ist die genaue Syntax
- RICHTIG: std::unique_ptr<char[]> name_;
- FALSCH: std::unique_ptr<char>name_;
- in beiden Fällen würde der `std::unique_ptr` intern einen einen `char*` enthalten, aber nur im ersten Fall wird der Destruktor des `std::unique_ptr` die Freigabe mit `delete[]` vornehmen;
Kleinere Demos Dienstag
- https://godbolt.org/z/75eEEK8ce
"Starker[*] Datentyp `Length` mit Umwandlung in unterschiedliche Maßeinheiten
[*: stark = nicht DIREKT kompatibel mit `double`] - https://godbolt.org/z/5vMbfKcve
Non-copyable Klasse durch Löschen des automatisch erzeugten `operator=` - https://godbolt.org/z/Pa1s5snjf
Vergleich zur Operator-Überladung als Member-Funktion und als globale Funktion) - https://godbolt.org/z/aYKjzqaxf
Typumwandlungen (eingangsseitig, ausgangsseitig, mit/ohne expicit)
Weitere Links Dienstag
- https://github.com/tbfe-de/FPGA-Kongress_2019
Vortragsunterlagen "Down to Earth C++" (ursprünglich FPGA-Kongress 2019) u.a. mit dem Beispiel zu "starken Datentypen" (hier: für Strom, Spannung und Leistung);
der grüner Button Code (rechts oben) erlaubt- Download als zip-File oder
- Klonen als Git-Repository
Aufgabe Dienstag Vormittag
- Basierend auf der DEMO https://godbolt.org/z/5hTjMb8dY ergänzen Sie die letzte Fassung von `NamedCounter` in https://godbolt.org/z/MxTYf3Yvj um folgende Features:
- Ausgabe-Operator, der den Namen gefolgt vom Wert ausgibt.
- Präfix-Inkrement (Rückgabe Referenz auf verändertes Objekt)
- Postfix-Inkrement (Rückgabe Kopie des Objekts im vorherigen Zustand
- HINWEIS: Am besten schreibt man hierfür einen Konstrukor, der einen NamedCounter aus ZWEI Argumenten erstellt: dem Namen und dem Wert
- Damit lässt sich das Postfix-Inkrement wie folgt implementieren:
NamedCounter operator++() {
return NamedCounter{getName(), ++value_;
}
- Subtraktion zweier `NamedCounter` als globale Funktio
int operator-(NamedCounter const& lhs, NamedCounter const& rhs)
den Abstand zwischen den Zählerständen liefert. - Zuweisung ganzzahliger Werte an einen `NamedCounter` als Member-Funktion
NamedCounter& operator=(int rhs) .... // TBD
der (unter Beibehaltung des Namens) den Zählerstand auf den neuen Wfert setzt. - Einen weiteren Zuweisungs-Operator als Member-Funktion
NamedCounter& operator=(NamedCounter const& rhs) .... // TBD
der vom `NamedCounter`-Objekt auf der rechten Seite der Zuweisung ebenfalls nur dessen Wert als neuen Wert übernimmt aber seinen Namen beibehält.- HINWEIS: Sie können innerhalb der Implementierung dieses Zweisungsoperators gerne den Zuweisungsoperator aus dem vorherigen Schritt wiederverwenden.
- https://godbolt.org/z/4sc5Gs1od teilweise und
https://godbolt.org/z/3xjnvTd87 vollständige Musterlösung inkl. erweiterter Testunterstützung durch einen- Makro `PX_` welcher eine Ausgabe nur dann erzeugt, wenn diese vom Erwartungswert abweicht, der als weiteres Argument übergeben wird.
- Am Beispiel dieses Makros wird ferner die
do {\
.....\
} while(0)
Technik als Alternative zu einem komplexen `(void)(.....)`-Ausdruck demonstriert.
Aufgabe Dienstag Nachmittag
- Teilen Sie den `NamedCounter` auf in
- eine Klasse `Counter`, welche nur die Funktionalität des Zählens enthält,
- eine Klasse `NamedCounter`, welche einen einen Namen hinzufügt
- https://godbolt.org/z/sfjT8M177 <---- Vorgabe zu dieser Übung, mit drei Alternativen:
- `Counter` als öffentliche Basisklasse von `NamedCounter`
- `Counter` als private Basisklasse von `NamedCounter`
- `Counter` als Daten-Member von `NamedCounfer`
Kleinere Demos Mittwoch
- https://godbolt.org/z/cv1fo5588
Aufteilung des NamedCounter in zwei Klassen (`Counter` und `NamedCounter`) verküpft mittels Vererbung (mit öffentlicher Basisklasse) und Komposition (mit privater Basisklasse sowie Member-Datenelement) - https://godbolt.org/z/3xjnvTd87
`Counter` / `NamedCounter` mit `operator<<` und dessen Test mittels `std::ostringstrem` - https://godbolt.org/z/GY67aErzY
`Counter` / `NamedCounter` mit `operator>>` implementiert als Template - https://godbolt.org/z/x4aEe7fqj
`ChainCounter`-basierte 24-Stunden Digitaluhr - https://godbolt.org/z/zKbG8MxKo
Counter / NamedCounter verknüpft mittels Templates
(enthält ferner Ersatz komplexer Präprozessors durch Inline-/Template-Funktionen und einen Call-Wrapper Makro, der sich auf das beschränkt, was nur per Makro machbar ist)
Weitere Links Mittwoch
- https://isocpp.org/wiki/faq/proper-inheritance
Absolute Lese-Empfehlung! Vermittel sehr viel an Einsicht, wann Vererbung (also public Biasisklassen) wirklich sinnvoll ist und wann nicht oder zumindest nicht als erste Wahl. - Eigene Infografiken (ergänzend zur Schulungsunterlage):
Aufgabe Mittwoch
- https://godbolt.org/z/hE3Eos83E <---- Vorgabe
Implementieren Sie basierend auf der Klasse `ChainCounter` eine Zählerkette im Stil einer 24-Stunden Digital-Uhr (Stunden, Minuten, Sekunden).- die unteren beiden Zählerstufen müssen über den `next_`-Zeiger mit ihrem jeweiligen Nachfolger verbunden werden
- in der obersten Zählerstufe muss der `next_`-Zeiger `ein nullptr` sein
- Im Falle des Überlaufs (Zähler wird beim Erreichen von `limit_` auf 0 gesetzt) muss `incr()` die verbundene Zählerstufe – wenn vorhanden – einen Schritt zählen lassen.
- Die Lösung soll mindestens folgende Tests bestehen.
Clock24 clk; PX_("00:00:00", clk.to_string());
clk.tick(1); PX_("00:00:01", clk.to_string());
clk.tick(100); PX_("00:01:41", clk.to_string());
clk.tick(3600); PX_("01:01:41", clk.to_string());
int const full = 24*3600 - 3600 - 100 - 1;
clk.tick(full); PX_("00:00:00", clk.to_string());
clk.tick(1); PX_("00:00:01", clk.to_string());
- Verwenden Sie zur alternativen Implementierung der 24-Stunden Digital-Uhr die beiden Klasse `Counter` und `OverflowCounter`:
https://godbolt.org/z/P4d5fvfb7 <---- Lösung des vorhergehenden Schritts
(kann ggf. auch as Vorgabe des nächsten Schritts dienen)
https://godbolt.org/z/Mr3zMfTE5 <---- Vorgabe mit ausführlichen TBDs- Die erste soll eine öffentliche Basisklasse der zweiten sein
- Die zweite soll die von der ersten Schritt verwendet werden geerbte Member-Funktion `incr` wie folgt überschreiben:
- Zunächst wird die geerbte Implementietung aufgerufen: `BaseCounter::incr()`
- Wurde dadurchder Zählerwert auf 0 zurückgesetzt, wird die als nächtste verbundene Zählerstufe inkrementiert:
`next.incr()` - Um Festzutstellen, ob in der eigenen Basisklasse der `value_` auf 0 zurückgesetzt wurde, kann dieser dort ` protected` gemacht werden. (Alternativ könnte das Ergebnis von `to_string()` mit "00" verglichen werden.)
- Es sind die selben Testfälle wie für den ersten Schritt verwendbar nur die Klassennamen müssen entsprechend angepasst werden.
- Zusatzfrage: Für die 24-Stunden-Uhr aus drei Stufen ist es wichtig, dass die `incr`-Memberfunktion `virtual` ist. Warum fällt das Problem, welches ansonsten entsteht, in einer Zählerkette aus nur zwei Stufen noch nicht auf?
- https://godbolt.org/z/deq1K9ofa <---- Lösung des vorhergehenden Schritts
(kann ggf. auch as Vorgabe des nächsten Schritts dienen)
https://godbolt.org/z/3zaq5G1W6 <---- Vorgabe mit ausführlichen TBDs
Implementieren Sie `BaseCounter` und `LimitCounter` nun unter Anwndung des NVI-Idioms (Non-Virtual-Interface):- Die `incr()`-Member-Funktion – ist in `Counter` öffentlich und soll daher in der abgeleiteten Klasse `OverflowCounter` nicht überschrieben werden und in `BaseCounter` nicht `virtual` sein.
- Stattdessen soll der `Counter` eine (neue) private Member-Funktion `overflowed()` erhalten, welche aus `incr()` immer dann aufgerufen wird, wenn der Zählerwert auf 0 zurückgesetzt wurde.
- Nur diese Memberfunktion soll dann virtuell sein und von der abgeleiteten Klasse überschrieben werden.
- In der Basisklasse ist `overflowed()` eine leere Funktion.
- In der abgeleiteten Klasse zählt ruft sie `next_.incr()` auf.
- https://godbolt.org/z/o7frqMcWa <---- Lösung des vorhergehenden Schritts
(kann ggf. auch as Vorgabe des nächsten Schritts dienen)
Die 24-Stunden Digital-Uhr soll nun zwei neue Funktionalitäten bekommen:- auf den Tagesanfang (Mitternacht = 00:00:00) zurückgestellen
- auf eine beliebige Zeit stellen (dies soll direkt erfolgen, nicht durch schnelles hoch-/herunterählen, wie man es von älteren Digitalweckern kennt)
- Erwägen Sie zunächst mehrere Lösungsalternativen, bevor Sie eine davon implementieren, z.B.
- Soll das Setzen eines beliebigen Zählerstands direkt erfolgen, braucht die Zählerklasse offensichtlich ein `set(int)` zum Einstellen eines beliebigen Zählerstands.
- Sollte man dieses auch zum Rücksetzen auf 0 verwenden oder dafür eher eine eigene Funktion `reset()` vorsehen.
- Was spricht für und was gegen die Zusammenfassung von `set()` und `reset()` in zu einer einzigen Funktion in der das Argment den Default-Wert 0 hat?
- Sollte eine in der Basisklasse `BaseCounter` eingeführte `reset()`-Member-Funktion in den abgeleiteten Klasse so implementiert werden, dass sie auch die verbundene nächste Zählerstufe auf Null setzt?
- Was bedeutet letzteres für eine Umsetzung im Sinne des NVI-Idioms?
- Wenn es zur Implementierung kommt, müssen Sie nicht ganz exakt einem der obigen Vorschläge folgen. Sie können diese auch miteinander kombinieren oder völlig andere (eigene) Ideen weiter verfolgen.