Ich programmiere einen STM32 in C++. Als erstes soll die Hardware
initialisiert werden. Damit diese Methode vor der Initialisierung aller
anderen Objekte meiner Initializer Liste ausgeführt wird, habe ich diese
bis jetzt in eine eigene Klasse gepackt nur mit dieser einen Methode als
Konstruktor. Das sieht dann so aus:
Schön dass du Klassen magst, aber hier brauchst du doch gar keine.
Mach einfach die Initialisierung und dann den Rest und gut ist.
Ansonsten macht es das doch schon so wie du willst. Als erstes wird der
Konstruktor von Hardware gerufen.
Thomas W. schrieb:> Schön dass du Klassen magst, aber hier brauchst du doch gar keine.> Mach einfach die Initialisierung und dann den Rest und gut ist.
Und wie genau? Etliche Objekte haben keinen Defaultkonstruktor und
müssen in der Initializer Liste stehen, benötigen aber zuvor einen
Aufruf der Hardware() Methode.
Und nur damit diese vor den anderen Objekten ausgeführt wird, habe ich
hier diese Methode in einen Konstruktor einer eigenen Klasse gemacht,
die sonst nichts kann, außer ihren Kontruktor aufzurufen.
Vereinfacher schrieb:> Kann ich die Methode Hardware::Hardware() irgendwie in MeinProjekt> integrieren und diese als erstes ausführen lassen?
Du könntest die Methode halt auch in der Initializer-Liste aufrufen, und
damit eine Dummy-Variable initialisieren...
Dein Problem ist aber irgendwie ein anderes. meinHardware und meinSPI
passen nicht so richtig in dieselbe Liste. Irgendwie ist meinHardware
eher eine Etage weiter oben.
Eventuell solltest du die Hardware-Klasse einfach beerben? Dann würde
dein Aufruf einer Methode zum Aufruf des Konstruktors der Basisklasse:
Vereinfacher schrieb:> Und wie genau? Etliche Objekte haben keinen Defaultkonstruktor und> müssen in der Initializer Liste stehen, benötigen aber zuvor einen> Aufruf der Hardware() Methode
Indem du die Funktion am Anfang von main aufrufst?
Aber so wie du es machst geht auch. Nur einfacher: Wenns die erste
member variable ist braucht sie gar nicht in der Initializer Liste
stehen und der Default ctor wird trotzdem zuerst aufgerufen. Du kannst
die Klasse auch verschachtelt definieren. Oder einen Dummy member mit
dem Returnwert einer static member function initialisieren.
Funktionale Abhängigkeit sollte überhaupt nicht im Konstruktor
abgebildet sein. Er sollte lediglich das Objekt initialisieren.
Eine nötige Reihenfolge damit abzubilden ist unübersichtlich.
Wenn, dann haben die Klassen ein init() das danach in einer bestimmten
Reihenfolge gerufen wird.
Thomas W. schrieb:> Wenn, dann haben die Klassen ein init() das danach in einer bestimmten> Reihenfolge gerufen wird.
Soll ich also wieder auf C ausweichen, obwohl ich C++ nutze? Jedes
meiner Objekte wie SPI, USART, etc. hat keinen defaultkonstruktor.
Dadurch stelle ich sicher, dass alle diese Objekte beim Erstellen
bereits initialisiert sein müssen: USART meineUSART(USART2, 9600);
Das ist auch weiter kein Problem und ich finde ich völlig schlüssig und
auch ok, dass ich das über die Initializer Liste aufrufen muss. Durch
die Reihenfolge werden auch die Abhängigkeiten untereinander
sichergestellt. Nur dass ich Hardware() für einen Methodenaufruf extra
als Hardware-Konstruktor anlegen muss finde ich eine unschöne Lösung.
Aber ok, scheinbar ist das schon er eleganteste Weg.
Sven P. schrieb:> Hast du überhaupt gelesen und/oder verstanden, was ich geschrieben habe?
Ja. In deinem Beispiel habe ich aber immernoch für diese eine Methode
eine extra Klasse nur mit Konstruktor angelegt. Ob ich diese nun erbe
oder als Objekt anlege und in die Initializer Liste packe ist doch egal?
Ich würde ja gerne Hardware() in meinProjekt als normale Methode
integrieren und in der InitializerListe als erstes diese Methode
ausführen lassen.
Ich halte es für immer noch falsch das die Reihenfolge in der
initialisierungsliste der Membervariablen eine Bedeutung hat. Das ist
verwirrend und später nicht nachvollziehbar.
Wenn es Abhängigkeiten in der Reihenfolge gibt, also dass Hardware
zuerst initialisiert werden muss, dann sollte das sichtbar sein und
nicht in einer versteckten Reihenfolge in der initialisierungsliste.
Mit C vs C++ hat das erstmal nix zu tun, auch in c++ arbeitet ein
Programm noch sequenziell.
Thomas W. schrieb:> Mit C vs C++ hat das erstmal nix zu tun, auch in c++ arbeitet ein> Programm noch sequenziell.
Aber wo ist der Unterschied, ob ich in der Initializer List "Hardware(),
meinSPI(...), meinUSART(...), ..." schreibe oder im Konstruktor
1
Hardware();
2
meinSPI.init(...);
3
meinUSART.init(...);
Ich habe in beiden Fällen eine Reihenfolge vorgegeben. Sollte die
Reihenfolge in der Initializer Liste nicht mit der Reihenfolge in der
Klassendefinition übereinstimmen gibts sowieso eine Warnung, also alles
paletti.
Vorteil Initializer Liste: Ich benötige keine Defaultkonstruktoren und
kann mir das C-like .init() sparen.
Vorteil Kostruktor: Die Reihenfolge in der Klassendefinition ist egal.
Ich habe lieber einen eindeutigen Konstruktor (der erzwingt, dass die
Objekte beim Erstellen bereits voll umfänglich initialisiert sind) und
erspare mit das .init();
Vereinfacher schrieb:> Aber wo ist der Unterschied, ob ich in der Initializer List "Hardware(),> meinSPI(...), meinUSART(...), ..." schreibe oder im Konstruktor>>
1
>Hardware();
2
>meinSPI.init(...);
3
>meinUSART.init(...);
4
>
>> Ich habe in beiden Fällen eine Reihenfolge vorgegeben. Sollte die> Reihenfolge in der Initializer Liste nicht mit der Reihenfolge in der> Klassendefinition übereinstimmen gibts sowieso eine Warnung, also alles> paletti.
Warnung gibt es nur wenn die Reihenfolge der Initialisierung nicht mit
der Reihenfolge der Membervariablen in der Klasse übereinstimmt. Die
Definitinsreihenfolge der Klassen ist egal. (Können in beliebiger
Ordnung in anderen Modulen dazugelinkt werden)
> Vorteil Initializer Liste: Ich benötige keine Defaultkonstruktoren und> kann mir das C-like .init() sparen.> Vorteil Kostruktor: Die Reihenfolge in der Klassendefinition ist egal.
Defaultkonstruktor gibt es immer. Wenn du ihn nicht hinschreibst, macht
es der Compiler automatisch.
> Ich habe lieber einen eindeutigen Konstruktor (der erzwingt, dass die> Objekte beim Erstellen bereits voll umfänglich initialisiert sind) und> erspare mit das .init();
Verstehe ich nicht. Du kannst das init von Hardware doch in dem
Konstruktor, der Hardware verwendet und besitzt aufrufen.
Ich würde es so schreiben:
class Hardware {
public:
Hardware() {
// here initialize Hardware
}
};
class MeinProjekt{
public:
MeinProjekt();
private:
Hardware meinHardware;
SPI meinSPI;
};
MeinProjekt::MeinProjekt()
{
meinHardware(); // first initialize Hardware
meinSPI(SPI1, 1);
}
Thomas W. schrieb:> Defaultkonstruktor gibt es immer. Wenn du ihn nicht hinschreibst, macht> es der Compiler automatisch.
Das ist falsch.
Thomas W. schrieb:>
mh schrieb:> Thomas W. schrieb:>> Defaultkonstruktor gibt es immer. Wenn du ihn nicht hinschreibst, macht>> es der Compiler automatisch.>> Das ist falsch.
"If no user-declared constructors of any kind are provided for a class
type (struct, class, or union), the compiler will always declare a
default constructor as an inline public member of its class."
Thomas W. schrieb:> mh schrieb:>> Thomas W. schrieb:>>> Defaultkonstruktor gibt es immer. Wenn du ihn nicht hinschreibst, macht>>> es der Compiler automatisch.>>>> Das ist falsch.>> "If no user-declared constructors of any kind are provided for a class> type (struct, class, or union), the compiler will always declare a> default constructor as an inline public member of its class."
"If no user-declared constructors of any kind are provided"
ist nicht das gleiche wie
"Defaultkonstruktor gibt es immer."
Die "einfachste" Möglichkeit ist deine Initialisierung als erstes in
main() durchzuführen. Wenn das ein kleines Projekt ist, dann ist diese
einfache Lösung oft auch die beste.
Wenn du wirklich die Abhängigkeit von der "Hardware" als Klasse abbilden
möchtest, dann müssen alle abhängigen Funktionen auch entsprechend auf
das Hardware Objekt zugreifen. Wenn du bspw. Speicher brauchst, dann
muss es die Funktion Hardware::malloc geben. Wenn du auf den Pwm Kanal
#3 zugreifen willst, dann muss das über die Funktion Hardware::pwm_3
passieren, usw. usf.
Als Konsequenz musst du einen Verweise auf das Hardware Objekt durch
deine gesamte Implementierung durchreichen. Es ergibt sich von selbst,
dass das Hardware Objekt am Anfang der Kette erzeugt werden muss.
In der Praxis wird statt des Durchreichens häufig ein Singleton
genommen. D.h., dem Hardware Objekt (wovon es nur eins geben kann) ist
eine globale Variable vorgeschaltet die beim Zugriff prüft, ob das
Objekt bereits erzeugt wurde (...und es so automatisch beim ersten
Zugriff erzeugt).
Thomas W. schrieb:> Ich nochmal, so sieht es für mich eigentlich recht hübsch aus:
Und wo wird das Hardware() aufgerufen?
Mikro 7. schrieb:> Die "einfachste" Möglichkeit ist deine Initialisierung als erstes in> main() durchzuführen. Wenn das ein kleines Projekt ist, dann ist diese> einfache Lösung oft auch die beste.
Das ist aber wieder C-like.
In meiner main() steht nur folgendes und so soll das auch bleiben:
1
#include"meinProjekt.hpp"
2
3
meinProjektmeinProjekt;
4
5
intmain(void){
6
while(true){
7
meinProjekt.run();
8
}
9
10
return0;
11
}
Es ist also alles in Klassen gekapselt, ebenso die Oberklasse vom
Projekt selber. Frei im Raum stehende Funktionen gibt es nicht.
Aber da ich keine einfache Antwort bekommen habe schätze ich, dass es
nicht möglich ist was ich wollte.
Danke trotzdem.
Vereinfacher schrieb:> Das ist aber wieder C-like.
Nein, C++ ist eine Multi-Paradigmen-Sprache und kein Java.
Vereinfacher schrieb:> Es ist also alles in Klassen gekapselt, ebenso die Oberklasse vom> Projekt selber. Frei im Raum stehende Funktionen gibt es nicht.
Nein, es ist alles in Klassen. (Punkt)
Gekapselt ist da nichts, außer du hast Clock, Interrupts, DMA und
Pin-Belegung auch schon bedacht.
Vereinfacher schrieb:> Thomas W. schrieb:>> Ich nochmal, so sieht es für mich eigentlich recht hübsch aus:>> Und wo wird das Hardware() aufgerufen?
Das wird automatisch aufgerufen bei Konstruktion der Basisklasse.
https://godbolt.org/z/K8j7qT
Bin aber nicht sicher ob das überhaupt das ist was du wolltest.
Vereinfacher schrieb:> Thomas W. schrieb:> Und wo wird das Hardware() aufgerufen?
Durch Vererbung. (in diesem Fall imho aber ein Anti-Pattern)
> Mikro 7. schrieb:> Das ist aber wieder C-like.
Ich habe geschrieben wie du die Abhängigkeit von der Hardware
modellieren kannst. Und das ist super-duper C++ Style. ;-)
Mikro 7. schrieb:> Vereinfacher schrieb:>> Thomas W. schrieb:>> Und wo wird das Hardware() aufgerufen?>> Durch Vererbung. (in diesem Fall imho aber ein Anti-Pattern)
Warum? Es bildet die Abhängigkeit der Peripherie von der Hardware sauber
ab.
Jedenfalls besser, als der krampfhafte Versuch, zwei eigentlich
verschachtelte Aspekte nebeneinander anzuordnen.
Vereinfacher schrieb:> Und wo wird das Hardware() aufgerufen?
Na erzeugst/instanziierst du nicht ein Objekt von der Klasse 'Hardware'
namens 'meinHardware' in einem Objekt der Klasse 'MeinProjekt' als
erstes in deiner Konstruktorliste?
Da wird doch dein Konstruktor aufgerufen. Ist halt zustandsloses Objekt.
> Das ist aber wieder C-like.
Was willste eigentlich? C++ programmieren mit Objektorientierung. Na
gut, dann mach doch einfach. Reihenfolge deiner Konstruktorliste
bestimmt die Reihenfolge der Initialisierung des Objekts, fertig ist.
Und halt RAII machen, Konstruktoren initialisieren, Destruktoren räumen
auf, abhängige Objekte per Referenz reinschleifen. Fertig.
> In meiner main() steht nur folgendes und so soll das auch> bleiben:#include "meinProjekt.hpp">> meinProjekt meinProjekt;>> int main(void){> while(true){> meinProjekt.run();> }>> return 0;> }
Hmm.
> Es ist also alles in Klassen gekapselt, ebenso die Oberklasse vom> Projekt selber. Frei im Raum stehende Funktionen gibt es nicht.
Recht dogmatische Herangehensweise.
> Aber da ich keine einfache Antwort bekommen habe schätze ich, dass es> nicht möglich ist was ich wollte.
Du hast doch was du willst. Trace doch einfach mal deine
Funktionsaufrufe in deine Debugkonsole oder im
Simulator/Emulator/irgendwohin. Fertsch ist.
Sven P. schrieb:>> Durch Vererbung. (in diesem Fall imho aber ein Anti-Pattern)>> Warum? Es bildet die Abhängigkeit der Peripherie von der Hardware sauber> ab.> Jedenfalls besser, als der krampfhafte Versuch, zwei eigentlich> verschachtelte Aspekte nebeneinander anzuordnen.
Vererbung? Och Leute, ey, wirklich? Bloss weil Vererbung im realen Leben
Spass machen kann, wieso soll das was Gutes in der Programmierung sein.
Was macht denn der Compiler bei Vererbung? Er legt implizit ne Art
unbenannte/unsichtbare class/struct-Member von der Basisklasse in der
Ableitung an, für Basisklassencasts:
1
class Widget
2
{
3
public:
4
int m_x, m_y;
5
};
6
7
class Slider : public Widget
8
{
9
int m_value;
10
public:
11
Slider() : Widget(), m_value(0) {}
12
};
Warum ist das Moppelkotze? Zum einen Liskov, zum anderen, weil man der
Ableitung immer die Implementierung der Basisklasse auf die Backe
bindet. Implementieren gegen Schnittstellen mit virtueller Vererbung
kannste jedenfalls den Hasen geben, das ist brotlose Kunst.
Und weil explizit halt immer vor implizit geht, isses halt geil, wenn
man Komposition gegenüber Inheritance vorzieht:
1
class Widget
2
{
3
public:
4
int m_x, m_y;
5
};
6
7
class Slider
8
{
9
Widget m_widget;
10
int m_value;
11
public:
12
Slider() : m_widget(), m_value(0) {}
13
};
Haste das selbe, klemmst dir aber den ganzen Basisklassensprattel und
förderst Layer-Bildung. Also Favor-Composition-Over-Inheritance.
db8fs schrieb:> Warum ist das Moppelkotze? Zum einen Liskov, zum anderen, weil man der> Ableitung immer die Implementierung der Basisklasse auf die Backe> bindet.
Ich kenne das Liskov substitution principle, habe aber keine Ahnung was
du hier sagen willst. Kannst du etwas genauer erklären, was du hier mit
Liskov meinst und was hier wem auf die Backe gebunden wird?
db8fs schrieb:> Und weil explizit halt immer vor implizit geht, isses halt geil, wenn> man Komposition gegenüber Inheritance vorzieht
Was ist hier expliziter als bei der Vererbung? Mal abgesehen von den
ganzen Funktionen, die man in Slider explizit neu implementiernen muss,
obwohl es sie in Widget schon mit gleicher Funktionalität gibt.
db8fs schrieb:> Haste das selbe, klemmst dir aber den ganzen Basisklassensprattel und> förderst Layer-Bildung. Also Favor-Composition-Over-Inheritance.
Was ist der "ganze Basisklassensprattel"? Wo wird hier Layer-Bildung
gefördert und wie?
db8fs schrieb:> Reihenfolge deiner Konstruktorliste bestimmt die Reihenfolge der> Initialisierung des Objekts, fertig ist.
Das stimmt so nicht!!!
Die Reihenfolge der Konstruktion wird allein durch die Reihenfolge der
Deklaration der Member bestimmt. Die Reihenfolge in der Initializerliste
ist egal.
Deshalb ist es auch sinnvoll das Warning einzuschalten, das prüft ob die
Reihenfolgen übereinstimmen. Dann ist man gezwungen beide Reihenfolgen
konsistent zu halten.
Detail dazu hier unter "Initialization order".
https://en.cppreference.com/w/cpp/language/constructor
Sven P. schrieb:> Mikro 7. schrieb:>> Durch Vererbung. (in diesem Fall imho aber ein Anti-Pattern)>> Warum? Es bildet die Abhängigkeit der Peripherie von der Hardware sauber> ab.> Jedenfalls besser, als der krampfhafte Versuch, zwei eigentlich> verschachtelte Aspekte nebeneinander anzuordnen.
Ich bin ein bisschen ambivalent bzgl. Vorgaben bei der Programmierung.
In diesem Fall ist es für mich aber ein deutliches Anti-Pattern
(https://en.wikipedia.org/wiki/Anti-pattern).
Grundsätzlich gilt, wenn möglich, Komposition der Vererbung vorzuziehen
(siehe HAS-A statt IS-A:
https://en.wikipedia.org/wiki/Composition_over_inheritance). -- Hier ist
es nicht mal ein HAS-A (sondern eine Referenz auf die Hardware, wenn man
die Unterscheidung im Design machen möchte).
Keine Regel ohne Ausnahme (bspw. beim Prototyping; wenn man für die
genutzten Funktionen der "Basisklasse" keine Wrapper schreiben möchte).
Hier sehe ich keinen guten Grund für eine Ausnahme.
Und es besteht sowieso das "Problem", dass die Hardware nicht in einer
Klasse gekapselt ist. Damit fehlt eh der "objektorientierte" Ansatz.
Für ein privates kleines Projekt ist die Diskussion eher belanglos:
"Mach' was für dich selbst am besten passt". Und dem scheint der TO auch
zu folgen. ;-)