Hallo C++ Experten,
für ein Mikrocontrollerprojekt mit dem AVR-GCC-Compiler in C++ habe ich
eine Klasse mit einem parametrisierten Konstruktor erstellt, von welcher
ein Objektarray in einer weiteren Klasse benutzt wird, hier ein
simplifiziertes Beispiel.
Die Objekte des Objektarrays "Array" der Klasse "ObjectB" müssen
natürlich konstruiert werden, was ich im Konstruktor der Klasse
"ObjectB" machen wollte, der Compiler jedoch als zu spät erachtet und
mit einer Fehlermeldung quittiert.
Ein Versuch, das Objektarrays "Array" in der Initialisierungsliste des
Konstruktorkopfes des Konstruktors der Klasse "ObjectB" zu
initialisieren, scheiterte ebenfalls stets an
Formulierungsfehlermeldungen. Die Suche nach einer Lösung im Internet
brachte mir die vage Vermutung, dass das Initialisieren eines
Objektarrays mit Parametern in einem Konstruktor so einfach gar nicht
möglich zu sein scheint.
Um das Initialisierungsproblem zu umgehen, habe ich kurzerhand den
Konstruktor der Klasse "ObjectA" parameterlos gestaltet und eine
Initialisierungmethode erstellt, die im Konstruktor der Klasse "ObjectB"
dann aufgerufen wird und "ObjectA" "manuell initialisiert".
Diese Lösung funktioniert nun zwar soweit, dennoch bezweifle ich ob es
sich dabei um die optimale Lösung handelt. Ich möchte Euch daher gerne
fragen, ob es tatsächlich nicht möglich ist ein parametrisiertes
Objektarray in einer Initialisierungsliste zu initialisieren und wie Ihr
dieses Problem lösen würdet.
Vielen Dank und beste Grüße,
Roland.
Mach Dir mal den Unterschied zwischen Initialisierung und Zuweisung
klar.
Und bei der Erzeugung eines Arrays wird bei nicht-primitiven DT der
std-ctor aufgerufen. Hat die Klasse Object einen std-ctor?
Hallo Wilhelm,
danke für Deine Antwort.
Die Klasse "ObjectA" hat im Fall des ersten (nicht funktionierenden)
Beispiels keinen Standardkonstruktor. Der Standardkonstruktor wird beim
Anlegen und gleichzeitigem Initialisieren eines Arrays aber nicht
aufgerufen oder benötigt, denn
1
ObjectAArray[]={{0},{1}};
lässt sich ohne vorhandenen Standardkonstruktor compilieren. Steht diese
Initialisierung direkt in der Definition von Klasse "ObjectB" lässt sich
das zwar ebenfalls compilieren, jedoch nur mit statischen Werten.
Roland .. schrieb:> Steht diese> Initialisierung direkt in der Definition von Klasse "ObjectB" lässt sich> das zwar ebenfalls compilieren, jedoch nur mit statischen Werten.
Code?
Hallo Roland,
der Code lässt sich nicht Compilieren, weil beim Eintritt in den
Konstruktor einer Klasse deren Member bereits initialisiert sind...
Dafür gibt es die Member Initializer List:
1
ObjectB::ObjectB(constuint8_tValueB)
2
:Array{{ValueB},{ValueB+1}}
3
{}
So sollte es funktionieren. Wenn man das nicht an dieser Stelle macht,
wird der Standard-Konstruktor aufgerufen, was du aber wahrscheinlich so
nicht haben möchtest.
Viele Grüße,
Frank
Frank schrieb:> Dafür gibt es die Member Initializer List:> ObjectB::ObjectB(const uint8_t ValueB)> : Array{ {ValueB}, {ValueB + 1} }> { }> So sollte es funktionieren.
Das führt zu einem Fehler, aber aus einem anderen Grund: ValueB + 1 ist
vom Typ int und müsste dann für die Initialisierung nach uint8_t
konvertiert werden, was aber dieser Form der Initialisierung nicht
implizit erlaubt ist, da es zu Informationsverlust führen kann. Man muss
die Konvertierung vorher explizit durchführen (und sich sicher sein,
dass es im Falle von ValueB==255 nicht zu Fehlverhalten kommt):
Rolf M. schrieb:> Das führt zu einem Fehler
Zunächst zu einer Warnung, nur bei -Werror zu einem Fehler.
Da er eh einen Typumwandlungs-ctor hat (wenn er denn so gewollt ist),
kann man auch einfach schreiben:
BTW: "naming is hard", deswegen würde ich mir auch in so einem MCVE mal
Gedanken über die Namen machen. Klassen sind nicht umsonst auch
"Namensräume".
Und noch was: bitte keinen Code abtippen. Das Beispiel des TO ist
einfach auch syntaktisch falsch.
Und noch was: bitte kein C-Arrays in C++, dafür nimmt man std::array<>.
Wilhelm M. schrieb:> Rolf M. schrieb:>> Das führt zu einem Fehler>> Zunächst zu einer Warnung, nur bei -Werror zu einem Fehler.
Da es um gcc geht, ja. Bei clang wäre es ein Fehler.
Rolf M. schrieb:> Wilhelm M. schrieb:>> Rolf M. schrieb:>>> Das führt zu einem Fehler>>>> Zunächst zu einer Warnung, nur bei -Werror zu einem Fehler.>> Da es um gcc geht, ja. Bei clang wäre es ein Fehler.
Das stimmt, wobei es lt. Standard ein diagnostic-required Fall ist. Eine
Implementierung wie gcc oder clang kann also entscheiden, was dort
produziert wird.
Roland .. schrieb:> Diese Lösung funktioniert nun zwar soweit, dennoch bezweifle ich ob es> sich dabei um die optimale Lösung handelt. Ich möchte Euch daher gerne> fragen, ob es tatsächlich nicht möglich ist ein parametrisiertes> Objektarray in einer Initialisierungsliste zu initialisieren und wie Ihr> dieses Problem lösen würdet.
Grundsätzlich stellt sich für mich die Frage, ob überhaupt ein uint8_t
in eine Klasse gewrappt werden muss, alleine schon wegen der ganzen
struct-packing-Fragestellung - müssteste bei Verwaltung in nem Array
ohne entsprechende pragmas viel unnötigen Speicher drin - wenn nicht
noch mehr Daten in ClassA gehalten werden sollen.
Die vorgestellte Lösung von Wilhelm mit dem {}-Initializern ist
vermutlich am nächsten dran, könnte man so machen. Musste eben
-Wno-gnu-array-member-paren-init als Schalter dem GCC mitgeben.
Da sollte sich vermutlich dann auch relativ gut ein template draus bauen
lassen, wenn keine STL-Container zur Verfügung stehen.
Ach so zur Begrifflichkeit noch: unter 'parametrisiert' verstehe ich
irgendwas mit template. z.B. parametrisierte Funktion halt halt per
template definierten Parametertyp. Auch die Unterscheidung Object/Klasse
ist nicht das selbe. Was du benennst als 'Object' ist zur Laufzeit
vielleicht eins, aber zur Kompilierzeit noch nicht - da isses bloß ne
Klasse, die eventuell auch noch nirgends verwendet wurde. Da find ich
die Begrifflichkeiten bissl wenig zielführend verwendet.
Wilhelm M. schrieb:> Und noch was: bitte kein C-Arrays in C++, dafür nimmt man std::array<>.
Hat avr-gcc das inzwischen?
Falk S. schrieb:> Grundsätzlich stellt sich für mich die Frage, ob überhaupt ein uint8_t> in eine Klasse gewrappt werden muss,
Es handelt sich um …
Roland .. schrieb:> ein simplifiziertes Beispiel.
Also wird da im Original wahrscheinlich noch etwas mehr stattfinden als
einfach nur ein uint8_t in eine Klasse gewrappt.
> alleine schon wegen der ganzen struct-packing-Fragestellung - müssteste> bei Verwaltung in nem Array ohne entsprechende pragmas viel unnötigen> Speicher drin - wenn nicht noch mehr Daten in ClassA gehalten werden> sollen.
Warum? Auf meinem PC ist sizeof(ObjectA) == 1, dann wird es das auf
einem avr auch sein. Ich wüsste auch nicht, warum es größer sein müsste.
> Ach so zur Begrifflichkeit noch: unter 'parametrisiert' verstehe ich> irgendwas mit template.
Ich verstehe darunter, Parameter mit einem Wert zu versehen. Ein
parametrisierter Konstruktor ist also einer, dem ich Parameter übergebe.
> Auch die Unterscheidung Object/Klasse ist nicht das selbe. Was du> benennst als 'Object' ist zur Laufzeit vielleicht eins, aber zur> Kompilierzeit noch nicht - da isses bloß ne Klasse, die eventuell auch> noch nirgends verwendet wurde. Da find ich die Begrifflichkeiten bissl> wenig zielführend verwendet.
Ich finde sie genau richtig, denn du benennst die Klasse sinnvollerweise
nach dem, was die Instanzen sind. Wenn du "ObjectA" in "ClassA"
umbenennst und dann schreibst:
1
ClassAx{3};
dann finde ich das nicht so ganz passend, denn ich erzeuge damit ja
keine Klasse, sondern ein Objekt.
Rolf M. schrieb:> Wilhelm M. schrieb:>> Und noch was: bitte kein C-Arrays in C++, dafür nimmt man std::array<>.>> Hat avr-gcc das inzwischen?
Es sollte doch inzwischen bekannt sein, daß Wilhelm produktiv mit C++42
arbeitet, und dazu eine vollwertige C++-Standardlib zur Verfügung hat.
Der Rest der Welt kann dafür z.B die hier nutzen:
https://github.com/modm-io/avr-libstdcpp
Oliver
Vielen Dank für Eure zahlreichen Antworten.
@Frank
Mit der von Dir vorgeschlagenen Schreibweise in der
Initialisierungsliste lässt sich der Code tatsächlich kompilieren und
das Objektarray initialisieren.
Ja, das habe ich bei meinen Versuchen bemerkt, dass der Compiler ohne
Angabe in der Initialisierungsliste den Standardkonstruktor aufrufen
möchte und dies zu einem Fehler führt, wenn dieser nicht vorhanden ist.
Daher wollte ich auch in der Initialisierungsliste die Parameter für das
Objektarray angeben, damit der Compiler den Konstruktor mit Parameter
aufrufen kann. Dazu habe ich alle möglichen Schreibweisen versucht,
jedoch ohne Erfolg. Mir kommt sogar vor, eine davon war auch die nun
funktionierende "Array{ {0}, {0} }". Möglicherweise hat die Größe des
Arrays nicht mit der Anzahl der Elemente in der Initialisierungsliste
zusammen gepasst. Jedenfalls lässt es sich jetzt fehlerfrei übersetzen,
danke.
@Rolf M.
Ja, das führt zu einer Warnung, eine Typenumwandlung beseitigt diese.
Danke für den Hinweis.
@Wilhelm M.
Damit meine ich folgendes, hat sich aber im Prinzip erübrigt:
1
classObjectB
2
{
3
private:
4
ObjectAArray[]={{0},{1}};
5
public:
6
ObjectB(constuint8_tValueB);
7
}
Nun, dass das erste Beispiel im Anfangsposting falsch ist, das liegt in
der Natur der Sache, das war ja der Grund der Frage. Aber es stimmt, im
zweiten vermeintlich funktionierendem Beispiel fehlen ebenso die
Semikolons bei den Klassen, die Methode "Init" hat keinen Rückgabewert
und "ValueA" darf nicht const sein, wenn sie über die Methode zugewiesen
wird. Übersimplifiziert sozusagen.
@Falk S.
Eigentlich bin ich überhaupt immer skeptisch wenn es um
objektorientierte Programmierung auf ganz kleinen Mikrocontrollern geht,
da im Hintergrund immer die Befürchtung mitschwingt, dass viel Overhead
erzeugt wird, weshalb ich stets auf reines C gesetzt habe. Als ich
einmal ein Projekt mit mehreren gleichen Strukturen – welche für
objektorientierte Programmierung prädestiniert sind – erstellt habe,
wollte ich C++ für den AVR einmal ausprobieren. Ich habe mir damals den
erzeugten Assemblercode näher angesehen und so ein klein wenig
verstanden wie der Compiler C++ Objekte umsetzt und war mit dem
erzeugten Overhead eigentlich zufrieden.
Mein nunmehriges Projekt besteht ebenfalls aus mehreren gleichen
Strukturen (Messwertspeicher und Berechnung) für die sich eine Umsetzung
in C++ anbietet. Insofern werden in den Klassen zwar nicht sehr viele,
aber doch mehr als eine Variable gehalten.
Okay, ja, mit parametrisiert war jetzt kein Template oder ähnliches
gemeint, sondern einfach ein Parameter der beim Aufruf übergeben werden
muss.
@Wilhelm M., @Falk S., Rolf M.
Ja, einer Klasse den Namen "Objekt" zu geben kann verwirrend sein, da
das Objekt(array) im Beispiel "Array" ist. Generell habe ich für mich
keine optimale Nomenklatur gefunden und mir kommt vor, jeder hat hier
seine eigenen Vorstellungen.
Ich konnte beispielsweise bis heute keine mir sympathische Bezeichnung
für eine Klasse finde, von der nur ein Objekt angelegt wird. Man findet
hier alle möglichen Schreibweisen, die ich als mehr oder weniger
gelungen erachte.
1
LCDLcd;
2
LCDLCD1;
3
LCD_LCD;
4
LCDLCDObject;
5
_LCDLCD;
6
LCDClassLCD;
Am ehesten sagt mir noch die letzte Variante zu, auch wenn durch den
Anhang "Class" der Klassename teilweise lang wird.
Beste Grüße,
Roland.
Roland .. schrieb:> Damit meine ich folgendes, hat sich aber im Prinzip erübrigt:class> ObjectB> {> private:> ObjectA Array[] = { { 0 }, { 1 } };> public:> ObjectB(const uint8_t ValueB);> }
Ich denke, die Fehlermeldung, die Du hier bekommst, sagt alles aus. Und
ja, in-class-initializer sind zu bevorzugen.
Roland .. schrieb:> @Wilhelm M., @Falk S., Rolf M.> Ja, einer Klasse den Namen "Objekt" zu geben kann verwirrend sein, da> das Objekt(array) im Beispiel "Array" ist. Generell habe ich für mich> keine optimale Nomenklatur gefunden und mir kommt vor, jeder hat hier> seine eigenen Vorstellungen.
Grundsätzlich gilt es, auch Redundanzen zu vermeiden. Deswegen hatte ich
den Hinweis mit dem Namensraum gegeben. Was ich meinte, ist, dass
Datenelement in dre Klasse ObjectA (mir war klar, dass dieser Name aus
der Natur des Beispiels war, obwohl ClassA gemeint war) nicht ValueA
genannt werden sollte. Das A in dem Namen ist redundant. Und der Name
Value hat keine Semantik, aber auch das kommt aus der Natur des MCVE.
Roland .. schrieb:> Ich konnte beispielsweise bis heute keine mir sympathische Bezeichnung> für eine Klasse finde, von der nur ein Objekt angelegt wird. Man findet> hier alle möglichen Schreibweisen, die ich als mehr oder weniger> gelungen erachte.> LCD Lcd;> LCD LCD1;> LCD _LCD;> LCD LCDObject;> _LCD LCD;> LCDClass LCD;
Achtung Begriffe: eine Klasse, von der nur ein Objekt angelegt werden
kann, ist etwas sehr besonderes: üblicherweise nach dem Muster Singleton
oder Monostate gebaut.
Aber in Deinem Beispiel hast Du ja die Klasse LCD von der Du 4 Objekte
anlegst. Du widersprichst Dir hier.
Am besten macht man eine Klasse, von der maximal ein Objekt angelegt
werden soll, uninstantiierbar und mit nur static Elementen. Ansonsten
kann man nämlich nur zur Laufzeit prüfen, ob mehr als ein Objekt
angefordert wird.
Roland .. schrieb:> Eigentlich bin ich überhaupt immer skeptisch wenn es um> objektorientierte Programmierung auf ganz kleinen Mikrocontrollern geht,> da im Hintergrund immer die Befürchtung mitschwingt, dass viel Overhead> erzeugt wird, weshalb ich stets auf reines C gesetzt habe.
Das geht wunderbar und produziert gar keinen Overhead. Man muss es
allerdings auch richtig machen, und ja, C++ ist eine
Multiparadigmen-Sprache mit viel mehr Möglichkeiten als C und damit auch
mehr Möglichkeiten, etwas "falsch" zu machen. Du wirst aber feststellen,
dass Du mehr Code wiederverwenden kannst und deswegen oft schneller am
Ziel bist.
Wilhelm M. schrieb:> Roland .. schrieb:> Achtung Begriffe: eine Klasse, von der nur ein Objekt angelegt werden> kann, ist etwas sehr besonderes: üblicherweise nach dem Muster Singleton> oder Monostate gebaut.
Wenn es von einer Klasse in einem Programm nur eine Instanz gibt, heißt
das nicht zwingend, dass es auch nur eine geben darf. Es kann ja auch
mal sein, dass man doch zwei LCDs anschließen will. Daher sollte man das
Singleton-Pattern mit Bedacht wählen und nur nutzen, wenn es tatsächlich
keinen Sinn ergibt, jemals mehr als eine Instanz zu haben.
Ich hab auch schon oft gesehen, dass Singletons gerne als vermeintlicher
Ersatz für globale Variablen verwendet werden, weil die ja "böse" sind.
Das ist aber auch nicht zielführend, weil die meisten Probleme, die
durch globale Variablen entstehen können, auch da sind, wenn man
stattdessen eine Singleton-Klasse als Ersatz verwendet.
> Aber in Deinem Beispiel hast Du ja die Klasse LCD von der Du 4 Objekte> anlegst. Du widersprichst Dir hier.
Er zeigt nur verschiedene Beispiele für die Namensgebung. Ich denke
nicht, dass er die alle gleichzeitig in einem Programm nutzen will.
Roland .. schrieb:> @Falk S.> Als ich> einmal ein Projekt mit mehreren gleichen Strukturen – welche für> objektorientierte Programmierung prädestiniert sind – erstellt habe,> wollte ich C++ für den AVR einmal ausprobieren. Ich habe mir damals den> erzeugten Assemblercode näher angesehen und so ein klein wenig> verstanden wie der Compiler C++ Objekte umsetzt und war mit dem> erzeugten Overhead eigentlich zufrieden.
Find ich auch super, dass du das ausprobierst. Ich meinte das eigentlich
bissl anders: wenn du bei C ne struct anlegst, definierst du ja nen
zusammengesetzten Datentyp. Klar ist es legitim, da auch nur einen
uint8_t reinzupacken - aber lohnen würde sich das imho erst, wenn die
Methoden dieser Klasse ne gewisse Verwaltungslogik mitbringen. Geht eher
bissl in Richtung sinnvolle Kapselung, wobei das bei kleinen uCs
manchmal gar nicht so einfach ist.
> Mein nunmehriges Projekt besteht ebenfalls aus mehreren gleichen> Strukturen (Messwertspeicher und Berechnung) für die sich eine Umsetzung> in C++ anbietet. Insofern werden in den Klassen zwar nicht sehr viele,> aber doch mehr als eine Variable gehalten.
Ok, dann hat sich das ja erledigt.
> @Wilhelm M., @Falk S., Rolf M.> Ja, einer Klasse den Namen "Objekt" zu geben kann verwirrend sein, da> das Objekt(array) im Beispiel "Array" ist. Generell habe ich für mich> keine optimale Nomenklatur gefunden und mir kommt vor, jeder hat hier> seine eigenen Vorstellungen.
Na ja, eigentlich ist es gar nicht so schwer, glaube ich. Der Typ einer
Variable ist ja auch was anderes als ihr Bezeichner. Und streng genommen
ist halt der Typ halt die Klasse und die Instanz der Klasse eben das
Objekt (-> die Variable).
> Ich konnte beispielsweise bis heute keine mir sympathische Bezeichnung> für eine Klasse finde, von der nur ein Objekt angelegt wird. Man findet> hier alle möglichen Schreibweisen, die ich als mehr oder weniger> gelungen erachte.
Es gibt Empfehlungen für sowas. Eine z.B. ist es, Klassennamen im
Singular zu schreiben und dafür ein Substantiv zu verwenden (keine
Substantivierung). Methoden sollten durch Verben beschrieben sein. Warum
im Singular? Weil du Mengen von Objekten ja in Container packen kannst.
> LCD Lcd;> LCD LCD1;> LCD _LCD;> LCD LCDObject;> _LCD LCD;> LCDClass LCD;>> Am ehesten sagt mir noch die letzte Variante zu, auch wenn durch den> Anhang "Class" der Klassename teilweise lang wird.
Der eigentliche Trick ist, glaube ich, zu gucken, was in der Klasse
eigentlich verwaltet bzw. getan wird und vielleicht auch, ob es Daten
sind oder Steuerlogik.
Die Namensgebung für LCD fänd ich jetzt auch nicht ideal, aber auch
schon nicht allzu schlecht.
Wenn deine Klasse hauptsächlich die SF-Register und Steuerlogik für ein
LCD enthält (und keine Daten), könnte der Name 'DisplayController' oder
'LCDController' geeignet sein. Auch 'Display' wär ok, wenn sonst keins
dran ist - kommt halt eben auf deine Appliance an. Wenn's eine Klasse
ist, die eher Einstellungen fürs LCD verwaltet (z.B aus EEPROM), kannst
du diese Funktionalität ja als Suffix anhängen, z.B. LCDLayout oder
DisplaySettings.
Zum Thema Singleton (also Zulassen von höchstens einem Objekt einer
Klasse):
besser bleiben lassen - lieber eine Klasse normal schreiben und erstmal
mit einer Instanz davon zufrieden sein, als die krampfhaft auf Singleton
umstellen zu müssen. Macht den Code nämlich nur in Ausnahmefällen besser
(z.B. beim Logging oder String-Konvertierung kann eine statische Klasse
nützlich sein).
@Wilhelm M.
Gut, in diesem Beispiel fehlt wider das Klassensemikolon. Ja, es wird ob
der fehlenden Größenangabe des Arrays eine recht eindeutige
Fehlermeldung generiert (too many initializers for 'ObjectA [0]').
Allerdings ist nicht ganz klar was du mir damit sagen möchtest. Sollte
das eine Anspielung auf meine Antwort an Frank sein, wo ich ausgeführt
habe, dass ich die funktionierende Lösung bei meinen Versuchen zwar
probiert habe, aber wohl an einer falschen Initialisierungslänge
gescheitert bin und ich nur die Fehlermeldung lesen hätte sollen, dann
muss ich dagegenhalten, dass die generierte Fehlermeldung bei größerer
Array- als Initialisierungslänge eine andere – für mich nicht
verständliche – ist (could not convert '<brace-enclosed initializer
list>()' from '<brace-enclosed initializer list>' to 'ObjectA').
Die Initialisierung in der Klasse selbst funktioniert ja nur mit Werten
die zur Compilezeit bekannt sind. Benötigt man aber Initialisierungen
deren Werte erst zur Laufzeit bekannt sind, geht das ja nur über die
Initialisierungsliste im Konstruktorkopf. Werden beide Arten benötigt,
sollte man die Initialisierung dann quasi aufteilen, die statischen in
der Klasse, die dynamischen in der Initialisierungsliste? Wirkt das dann
nicht etwas unübersichtlich?
Also das war dann wohl ein nicht ganz klar angeführtes Beispiel mit der
Benennung der LCD-Klasse und dessen Instanzen. Wie Rolf M. schon
angemerkt hat, sollten in dem Beispiel nicht vier Objekte der Klasse
"LCD" und jeweils eines der Klasse "_LCD" und "LCDClass" angelegt
werden, sondern das Beispiel sollte sechs unabhängige
Nomenklaturmöglichkeiten für Klasse und Objekt zeigen.
Ein interessanter Punkt eine Klasse mit nur einer benötigten Instanz
ausschließlich mit static-Elementen zu gestalten, wenn auch Rolf M. und
Falk S. dies kritisch sehen. Ich habe diese Konzept sogar schon in einer
anderen Programmiersprache (C#) verwendet.
@Falk S.
Ja, ist klar dass sich das Anlegen von gekapselten Strukturen erst ab
einer gewissen Mindestkomplexität lohnt.
Danke für das Ausführen der Nomenklaturlogik. Bei der Benennung von
Klassen habe ich das Prinzip eines Substantivs im Singular Größenteils
wohl intuitiv sogar verwendet. Nach welchem Schema sollen dann die
Objekte selbst benannt werden?
@Wilhelm M., Rolf M., Falk S.
Eine Klasse, die beispielsweise eine unikale Hardwareschnittstelle eines
Mikrocontrollers kapselt wäre also eigentlich prädestiniert für eine
Singletonklasse.
Roland .. schrieb:> @Wilhelm M.> Gut, in diesem Beispiel fehlt wider das Klassensemikolon. Ja, es wird ob> der fehlenden Größenangabe des Arrays eine recht eindeutige> Fehlermeldung generiert (too many initializers for 'ObjectA [0]').
In so einem Fall ist es am besten, wenn Du den Code samt der
Fehlermeldung und den Optionen für den Compiler angibts.
Roland .. schrieb:> Die Initialisierung in der Klasse selbst funktioniert ja nur mit Werten> die zur Compilezeit bekannt sind.
Jein.
> Benötigt man aber Initialisierungen> deren Werte erst zur Laufzeit bekannt sind, geht das ja nur über die> Initialisierungsliste im Konstruktorkopf.
Zumindest, wenn sie von den ctor-Parametern abhängen.
> Werden beide Arten benötigt,> sollte man die Initialisierung dann quasi aufteilen, die statischen in> der Klasse,
Statisch ist das nicht, sondern nur eine andere syntaktische
Möglichkeit.
> die dynamischen in der Initialisierungsliste? Wirkt das dann> nicht etwas unübersichtlich?
Nein. Für primitive DT sollte man immer in-class-initializer verwenden,
die ggf. durch die Initialisierungsliste ersetzt werden. Für
nicht-prinitive DT wie eh der std-ctor aufgerufen.
Roland .. schrieb:> Ein interessanter Punkt eine Klasse mit nur einer benötigten Instanz> ausschließlich mit static-Elementen zu gestalten, wenn auch Rolf M. und> Falk S. dies kritisch sehen.
Für mich der einzig richtige Weg, weil (interne) Peripherie immer nur in
begrenzter Vielfachheit vorhanden ist. Daher macht dort der simple OO
Ansatz keinen Sinn. Hier ist statische Polymorphie also das geeignete
Mittel. In C++ erreicht man statische Polymorphie mit Templates.
Roland .. schrieb:> @Wilhelm M., Rolf M., Falk S.> Eine Klasse, die beispielsweise eine unikale Hardwareschnittstelle eines> Mikrocontrollers kapselt wäre also eigentlich prädestiniert für eine> Singletonklasse.
Singleton hat so seine eigenen Probleme. Für mich wäre gerade noch ein
Monostate akzeptabel. Aber (s.o.) der beste Weg in meinen Augen ist ein
Template:
1
usingterminal=Uart<0,Position<Port<B>>>;
2
usinggps=Uart<1>;
3
// ...
Roland .. schrieb:> lso das war dann wohl ein nicht ganz klar angeführtes Beispiel mit der> Benennung der LCD-Klasse und dessen Instanzen. Wie Rolf M. schon> angemerkt hat, sollten in dem Beispiel nicht vier Objekte der Klasse> "LCD" und jeweils eines der Klasse "_LCD" und "LCDClass" angelegt> werden, sondern das Beispiel sollte sechs unabhängige> Nomenklaturmöglichkeiten für Klasse und Objekt zeigen.
Die Klasse LCD zu nennen, ist doch absolut ok. Wobei ich dazu tendieren
würde, es als LCD<4, 20> oder LCD<HD44780, Rows<4>, Columns<20>,
PinList> zu realisieren.
Hallo Wilhelm,
dank für Deine Ausführungen. Mit Templates habe ich mich bisher
eigentlich kaum beschäftigt und somit auch nicht verwendet. Der von Dir
eingebrachte Vorschlag klingt jedoch durchaus interessant. Ich werde mir
diese Technik aber genauer ansehen, möglicherweise finde ich für mich
auch weitere Einsatzmöglichkeiten von Templates.
Roland .. schrieb:> Danke für das Ausführen der Nomenklaturlogik. Bei der Benennung von> Klassen habe ich das Prinzip eines Substantivs im Singular Größenteils> wohl intuitiv sogar verwendet. Nach welchem Schema sollen dann die> Objekte selbst benannt werden?
Na ja, die Objekte selber beschreiben dann eben die tatsächlich
benutzbaren 'Entitäten', ich mach mal nen Beispiel:
Instanzen/Objekte können dann sehr konkret sein, für z.B. eine Flotte
von 3 Fahrzeugen:
1
CarvwPassatBlau;
2
CarminiRot;
3
Carkfz_J_XY_456;/*< z.B. Identität eines Autos per Nummerschild */
Hängt halt von dem modellierten Sachverhalt ab. In vielen
CodingGuidelines, z.B. Google oder QT wird auch empfohlen, sinnvolle und
einheitliche Groß/Kleinschreibungsregeln zu benutzen.
Hier hab ich mich bissl am Qt-Style angelehnt: Klassennamen groß,
Variablennamen klein und im CamelCase. Macht schlicht die Unterscheidung
zwischen Typebene (Klasse bzw. Typ) und Laufzeitebene bissl einfacher.
Kannste dann eben auch recht gut lesen:
1
CarvwKaefer;
2
3
vwKaefer.blinkeLinks();
4
vwKaefer.ausscheren();
5
vwKaefer.beschleunigeAuf(170);
6
vwKaefer.blinkeRechts();
7
vwKaefer.einfaedeln();
Haste dann relativ lesbaren Code, der den Anwendungsfall gut beschreibt
(hier z.B. Überholvorgang.
Wilhelm M. schrieb:> Roland .. schrieb:>> Ein interessanter Punkt eine Klasse mit nur einer benötigten Instanz>> ausschließlich mit static-Elementen zu gestalten, wenn auch Rolf M. und>> Falk S. dies kritisch sehen.
Na ja, dagegen hab ich da grundsätzlich nix, aber es ist halt potentiell
das Henne-Ei-Problem da: wer greift als erster drauf zu (denn der
erzeugt das Objekt). Und wenn mehrere statische Klassen wechelsseitig
aufeinander zugreifen kann das halt nicht ganz so erfreulich beim
Debuggen sein...
Daher würd ich empfehlen, statische Klassen bzw. Singletons am besten
nur zu benutzen, wenn die Klasse selber zustandslos ist, also am besten
keine wirklichen Member enthält (quasi lauter reentrant-Funktionen, die
keinen Zustand haben).
> Für mich der einzig richtige Weg, weil (interne) Peripherie immer nur in> begrenzter Vielfachheit vorhanden ist.
Das find ich als Argument nicht ganz so treffend. Du koppelst deinen
geschriebenen Code damit automatisch fix an die gegebene technische
Architektur -> eigentlich soll Code ja mit jeder Klasse, die eine andere
enthält/wrappt, ja von der Hardware weg abstrahiert werden, um die
Wiederverwendbarkeit zu erhöhen, wenn der Code mal woanders eingesetzt
wird.
Willst du den Code dann woanders hin portieren, wo es die selbe
Ressource 2x gibt, strickste deinen ganzen Code um.
Ist zugegeben bei 8-Bit-AVR vielleicht nicht ganz so tragisch, aber ganz
unwichtig ist es nicht, glaub ich.
> Daher macht dort der simple OO> Ansatz keinen Sinn. Hier ist statische Polymorphie also das geeignete> Mittel. In C++ erreicht man statische Polymorphie mit Templates.
Keep-It-Simple. Ich mag sauberen, gut verstehbaren Code, templates
setzen überall erstmal ne neue Abstraktionsebene drauf, die den
zusätzlichen Aufwand durch die parametrisierten Klassen/Funktionen auch
wert sein sollte. Für AVR kann's durchaus lohnend sein, gibt ja hier im
Forum schon paar nette Ansätze, die Peripherie der kleinen uCs bissl zu
abstrahieren.
Hallo Falk,
danke für das Beispiel. Das Unterscheiden von Klassen und Objekten
anhand der Groß- oder Kleinschreibung des ersten Buchstaben finde ich
interessant und gefällt mir eigentlich gut. Darauf habe ich bisher nicht
geachtet, eher versucht alle Namen, die länger als ein Zeichen sind, mit
einem Großbuchstaben zu beginnen.
Falk S. schrieb:> Kannste dann eben auch recht gut lesen:Car vwKaefer;> vwKaefer.blinkeLinks();> vwKaefer.ausscheren();> vwKaefer.beschleunigeAuf(170);> vwKaefer.blinkeRechts();> vwKaefer.einfaedeln();
Denglisch ;-)
Falk S. schrieb:> Na ja, dagegen hab ich da grundsätzlich nix, aber es ist halt potentiell> das Henne-Ei-Problem da: wer greift als erster drauf zu (denn der> erzeugt das Objekt). Und wenn mehrere statische Klassen wechelsseitig> aufeinander zugreifen kann das halt nicht ganz so erfreulich beim> Debuggen sein...
Du meinst wahrscheinlich das static-initialization-fiasko. Das ist nicht
nur unfreundlich beim Debuggen, sondern generell UB. Doch dafür gibt es
ja Abhilfe.
Falk S. schrieb:> Das find ich als Argument nicht ganz so treffend. Du koppelst deinen> geschriebenen Code damit automatisch fix an die gegebene technische> Architektur
Das ist zwingend so: ein UART auf STM32 ist anders als auf AVR.
> -> eigentlich soll Code ja mit jeder Klasse, die eine andere> enthält/wrappt, ja von der Hardware weg abstrahiert werden, um die> Wiederverwendbarkeit zu erhöhen, wenn der Code mal woanders eingesetzt> wird.
Genau, dafür schreiben wir ein Klasse: die Schnittstelle nach außen ist
die gleiche, intern ist es anders.
>> Willst du den Code dann woanders hin portieren, wo es die selbe> Ressource 2x gibt, strickste deinen ganzen Code um.
Nein.
> Ist zugegeben bei 8-Bit-AVR vielleicht nicht ganz so tragisch, aber ganz> unwichtig ist es nicht, glaub ich.
Das erscheint mir wirr.
Falk S. schrieb:> Keep-It-Simple. Ich mag sauberen, gut verstehbaren Code, templates> setzen überall erstmal ne neue Abstraktionsebene drauf, die den> zusätzlichen Aufwand durch die parametrisierten Klassen/Funktionen auch> wert sein sollte.
Nein.
Gerade die statische Polymorphie entspricht dabei vielmehr dem, was
modellirt werden soll. Und man kann eben viel mehr zur Compilezeit
prüfen. Laufzeitfehler sind in so einem Fall ünnötig und lästig.
> Für AVR kann's durchaus lohnend sein, gibt ja hier im> Forum schon paar nette Ansätze, die Peripherie der kleinen uCs bissl zu> abstrahieren.
Ob AVR oder STM32 oder ... ist dabei vollkommen egal.
Roland .. schrieb:> Hallo Wilhelm,>> dank für Deine Ausführungen. Mit Templates habe ich mich bisher> eigentlich kaum beschäftigt und somit auch nicht verwendet. Der von Dir> eingebrachte Vorschlag klingt jedoch durchaus interessant. Ich werde mir> diese Technik aber genauer ansehen, möglicherweise finde ich für mich> auch weitere Einsatzmöglichkeiten von Templates.
Du wirst sehr interessante und sinnvolle Möglichkeiten entdecken.
Natürlich findest Du auch in diesem Forum viel kontroverse Diskussion
dazu.
Allerdings möchte / kann nicht jeder die notwendige Abstraktionsleistung
dafür aufbringen: für manche Leute ist eben alles ein "int", und wenn
nicht, dann ein "String" ...
Ich möchte noch einmal auf das Thema
Singleton-Klasse/Klasse-mit-static-Elementen zurückkommen. Soweit ich es
verstanden habe, unterscheidet sich eine echte Singleton-Klasse von
einer Klasse, in der alle Elemente statisch sind. Sie ist mit dieser
typischen statischen "Instanz"-Methode, die eine Referenz oder einen
Zeiger zurückgibt, ein wenig umständlich wie ich finde, weshalb ich bei
meinem kleinen UART-Beispiel darauf verzichte.
Da von dieser Klasse keine Instanz angelegt wird (und soll), gibt es
entsprechend keinen Konstruktor. Das Einrichten und Aktivieren der
Schnittstelle muss also in einer eigenen Methode ("enable") stattfinden,
der beim Aufruf ein Wert für die Baudrate übergeben wird. Würde man zum
Einrichten keinen Parameter benötigen, wäre es dann möglich das
Einrichten automatisch erledigen zu lassen? Gibt es bei dieser Art von
Klassenform eine Art "Konstruktor-Methode" die automatisch aufgerufen
wird und in der man nötige Zuweisungen erledigen kann oder geht das
ausschließlich über eine eigene Methode die beim Programmstart manuell
aufgerufen werden muss?
Roland .. schrieb:> Da von dieser Klasse keine Instanz angelegt wird (und soll), gibt es> entsprechend keinen Konstruktor.
Doch, es gibt einen. Und der ist auch benutzbar, falls Du im eine
Implementierung gibts. Was ich nicht sehen kann, weil ich nicht weiß, ob
das obige vollständig ist.
Besser:
1
Uart()=delete;
Roland .. schrieb:> Würde man zum> Einrichten keinen Parameter benötigen, wäre es dann möglich das> Einrichten automatisch erledigen zu lassen?
Nicht direkt. Ich habe mir angewöhnt, eine init()-Funktion zu schreiben.
Da Du ja auch mehrere Uarts bestimmt verwenden möchtest, solltest Du
auch mal über so etwas nachdenken:
in mbed-os gibt es eine template class 'NonCopyable', von der kann
abgeleitet werden um die Kopier- und Zuweisungsoperation zu unterbinden:
https://github.com/mbed-ce/mbed-os/blob/master/platform/include/platform/NonCopyable.h
Die Benutzung ist einfach und elegant wie ich finde.
Einen UART als Singelton zu konstruieren halte ich für sehr daneben,
gerade solche Schnittstellen sind in µC mehrfach vorhanden.
J. S. schrieb:> in mbed-os gibt es eine template class 'NonCopyable', von der kann> abgeleitet werden um die Kopier- und Zuweisungsoperation zu unterbinden:
Damit ist die Kopierbarkeit ausgeschlossen.
Trotzdem muss die Klasse (HW Ressourcenverwalter) dann als Monostate
geschrieben werden. Wenn das der Fall ist, kann man das Kopieren aber
(meistens) auch wieder zulassen.
J. S. schrieb:> in mbed-os gibt es eine template class 'NonCopyable', von der kann> abgeleitet werden um die Kopier- und Zuweisungsoperation zu unterbinden:>> https://github.com/mbed-ce/mbed-os/blob/master/platform/include/platform/NonCopyable.h>> Die Benutzung ist einfach und elegant wie ich finde.> Einen UART als Singelton zu konstruieren halte ich für sehr daneben,> gerade solche Schnittstellen sind in µC mehrfach vorhanden.
Eine Uart<1> und Uart<2> wäre streng genommen auch ein Singleton. Wenn
ein Controller lediglich eine UART Schnitstelle besitzt sehe ich kein
Problem damit den Template Parameter gleich komplett wegzulassen.
Vincent H. schrieb:> J. S. schrieb:>> in mbed-os gibt es eine template class 'NonCopyable', von der kann>> abgeleitet werden um die Kopier- und Zuweisungsoperation zu unterbinden:>>>>> https://github.com/mbed-ce/mbed-os/blob/master/platform/include/platform/NonCopyable.h>>>> Die Benutzung ist einfach und elegant wie ich finde.>> Einen UART als Singelton zu konstruieren halte ich für sehr daneben,>> gerade solche Schnittstellen sind in µC mehrfach vorhanden.>> Eine Uart<1> und Uart<2> wäre streng genommen auch ein Singleton.
Ich würde es eher als statisches Monostate bezeichnen.
> Wenn> ein Controller lediglich eine UART Schnitstelle besitzt sehe ich kein> Problem damit den Template Parameter gleich komplett wegzulassen.
Wenn es etwas(!) allgemeiner werden soll, landest Du eh bei
Danke für Eure Antworten.
@A. B.
Ja, nur es muss ja "enable" (wenn es keinen Parameter benötigen würde)
erst wider einmalig aufgerufen werden. Also nicht dass das jetzt störend
wäre, ich wollte nur wissen, ob es eben für so einen Fall eine
vorgesehene Standardmethode (eine Art "static-Konstruktor") gibt, was
aber offensichtlich nicht der Fall ist.
Die Webseite finde ich im Übrigen spannend, alle möglichen Compiler
online verfügbar.
@ Wilhelm M.
Okay, dem nicht implementieren Konstruktor "delete" zuweisen anstatt in
als private zu deklarieren ist die richtige Lösung um ein Instanziieren
zu verhindern.
Ich habe mir nun die Grundlagen von Klassentemplates durchgelesen und
mir fällt sogar schon ein Anwendungsfall ein. Ich benötige eine Klasse,
die Messwerte verwaltet und darstellt. Die meisten Messwerte sind 16-Bit
breit, wenige 32-Bit breit. Jetzt müsste ich wegen der wenigen 32-Bit
breiten Messwerte die gesamte Klasse auf 32-Bit-Variablen auslegen. Hier
würde sich beispielsweise eine Klassentemplate anbieten, wenn ich das
Konzept richtig verstanden habe.
@J. S., Vincent H.
Sicher kommt es immer darauf an wie universell die ganze Klasse sein
soll. Nutzt man sie ausschließlich für AVRs sind mehr als eine UART auch
nicht unbedingt die Regel. Der Vorschlag die Klasse mit einer Vorlage zu
erstellen über die dann die jeweilige Schnittstelle ausgewählt wird,
scheint eine gute Möglichkeit zu sein.
Verzeiht mir mein erneutes Nachfragen, es haben sich bei meinen
überlegungen anhand der hier gewonnenen Erkenntnisse ein paar neue
Fragen aufgetan, die teilweise an die Ursprungsfrage anknüpfen.
Wie bereits erwähnt, möchte ich einige Werte auf einem Display
darstellen und dabei den Quellcode möglichst gut Kapseln. Dafür habe ich
mir eine "Info"-Klasse überlegt, die sich um die Darstellung der
Displayseite mit den Werten kümmert. Da es nur eine solche Seite mit
Werten gibt – also nur eine Instanz der Klasse benötigt wird – möchte
ich diese uninstanziierbar nur mit static-Elementen ausführen. Die Werte
selbst werden durch eine eigene normale Klasse ("ValueText") vertreten,
die sich um die Darstellung der Werte selbst kümmert. Den Namen
"ValueText" habe ich deshalb gewählt, weil diese Klasse eigentlich nur
die Basisklasse darstellt und Grundlagen der Werte behandelt, die jeder
Wert besitzt (Name des Wertes und Position). Von dieser Klasse soll
später die eigentliche "Werte-Klasse" abgeleitet werden, je nach
Wertekategorie (Zahlenwert, oder Wert dem ein Text zugeordnet wird). Im
Falle eines reinen Zahlenwertes möchte ich die abgeleitete Klasse mit
einem Template versehen um zwischen 16-Bit und 32-Bit wählen zu können.
Das folgenden Beispiel besitzt noch keine Implementierung der etwaigen
"Werte-Klasse" sondern die "Info-Klasse" enthält einfach Instanzen der
Basisklasse "ValueText", da sich hier schon meine Fragen ergeben:
Daraus ergeben sich für mich folgende Fragen, die ich gerne stellen
möchte:
In der Klasse "Info" gibt es zwei statisch Elemente der Klasse
"ValueText", eine Einzelvariable "value1" und ein Array "values". Obwohl
sich der Code fehlerfrei übersetzen lässt, funktioniert nur die Ausgabe
des Namens von "value1", das Array scheint nicht oder mit falschen
Werten initialisiert zu werden, die Ausgabe scheitert. Hier knüpft meine
Frage an die ursprüngliche Frage nach der Initialisierung eines
Objektarrays an.
Das Initialisieren der Variable "value1" und des Arrays "values" mit dem
jeweiligen Text-char-Array finde ich wenig elegant. Das Makro "PSTR"
lässt sich an dieser Stelle nicht nutzen, weshalb ich hier den Umweg
über die statischen Variablen "text1/2/3" wählen musste. Gibt es hier
eine bessere Lösung?
Sollen nun der zur Initialisierung von "value1" und "values" genutzte
Parameter "position" variabel sein und vom Wert "parameter" der
statischen Methode "Config" abhängig werden, ist das bei dieser Art der
Initialisierung ja nicht möglich. Benötigt man hierfür in der Klasse
"ValueText" eine eigene Methode die in "Config" von Klasse "Info"
aufgerufen wird und der Variable "position" einen Wert zuweist und
verzichtet auf das Initialisieren von "position" im Konstruktor sowie
auf das definieren als const?
Wird nun die eigentliche Werteklasse aus der Ableitung der
"ValueText"-Klasse erstellt und diese mit einem Template versehen, ist
es dann überhaupt möglich ein großes Objektarray dieser neuen Klasse in
der "Info"-Klasse zu erstellen? Oder können dann nur jeweils jene
Objekte ein Array bilden, die mit demselben Template-Parameter erstellt
wurden?
Vielen Dank für die Geduld und das Lesen,
beste Grüße,
Roland.
Roland .. schrieb:> Wird nun die eigentliche Werteklasse aus der Ableitung der> "ValueText"-Klasse erstellt und diese mit einem Template versehen, ist> es dann überhaupt möglich ein großes Objektarray dieser neuen Klasse in> der "Info"-Klasse zu erstellen? Oder können dann nur jeweils jene> Objekte ein Array bilden, die mit demselben Template-Parameter erstellt> wurden?
Bei einem homogenen Container wie eine rohes C Array oder std::array<>
müssen alle Elemente natürlich denselben Typ haben. Also std::array<A,
10>.
Instanziierte Templates, wie etwa B<int> sind ebenfalls konkrete Typen,
wobei dann B<int> und B<float> oder B<A> unterschiedliche Typen sind.
Man kann also nur ein std::array<B<int>, 10> oder std::array<B<A>, 10>
bilden.
Darüberhinaus gibt es allerdings auch heterogene Container wie
std::tuple<>. Oder Du nimmst ein std::variant<>.
Da Du aber auf dem avrgcc unterwegs bist fehlt Dir std::array,
std::tuple und std::variant. Um die selbst zu schreiben, muss man schon
etwas in die Materie einsteigen. Wobei std::array einfach ist und ein
gute Fingerübung, die sich lohnt.
Hallo Wilhelm M.,
vielen Dank für Deine Antwort.
Ja, das Element existiert nicht, das ist ganz eindeutig der Fehler,
diese Frage gerade ziemlich peinlich und ich suche nach einer Erklärung.
Ich habe keine Ahnung wieso mir das Offensichtliche nicht selber
aufgefallen ist, kann mir aber zumindest teilweise die Herkunft der "2"
erklären. Anfänglich habe ich als Array-Größe drei gewählt. Da die
Ausgabe aber wegen anderer Fehler nicht funktioniert hat, habe ich die
Schleife für weitere Tests mit dem festen Index 2 für das letzte
Arrayelement umgangen und es schließlich ohne Array mit der Variable
"value1" weiter versucht. Als die Ausgabe von "value1" dann endlich
funktioniert hat, habe ich das Array samt Ausgabeschleife wider in den
Code aufgenommen und gleichzeitig auf zwei Elemente gekürzt um die
Initialisierung kürzer zu halten. Die Anzahl der Schleifendurchläufe
wurde von mir zwar auf zwei reduziert, das jedoch als Index nach wie vor
die feste Zahl 2 stand und nicht die Schleifenvariable "i" habe ich
leider komplett übersehen und auch bei der Fehlersuche nicht und nicht
bemerkt. Wie gesagt peinlich, ich entschuldige mich für diese banale
Frage.
Wie schon vermutet müssen also mehrere Arrays mit jeweils gleiche Typen
gebildet werden. Dasselbe gilt vermutlich auch für Objekte von
abgeleiteten Klassen, die dann ja ebenso eigenständige Typen sind.
@Roland. Kannst du bitte erklären wie du da vorgehst. Sieht dein Code
einen Compiler? Compiliert dein Code? Gibt es Fehlermeldungen ?
https://godbolt.org/z/5M98E1sEW
Hallo A. B.,
ja, der Code lässt sich fehlerfrei compilieren und funktioniert nun dank
meiner Erleuchtung durch Wilhelm auch wie gedacht.
Um den Code im Onlinecompiler übersetzen zu können bedarf es einer
Anpassung, da der beschriebene Code nur die Klassen enthält und kein
vollständiges Programm darstellt.
Hierzu benötigt es neben der von Dir bereits eingebundenen IO-Bibliothek
auch jene für die Verwendung des Programmspeichers als Datenspeicher:
1
#include"avr/io.h"
2
#include"avr/pgmspace.h"
Da keine Definition des Datentyps "LCD" vorliegt und dieser Datentyp im
Beispiel auch nicht spezifisch verwendet wird, kann dies mit einer
simplen Definition als uint8_t gelöst werden:
1
#define LCD uint8_t
Abschließend braucht es noch die main-Funktion, wo die beiden
Klassenmethoden vom Ende des Beispiels aufgerufen werden:
1
intmain(void)
2
{
3
LCDLcd1;
4
Info::Config(&Lcd1,0);
5
Info::Draw();
6
}
Mit dieser Adaptierung lässt sich das Beispiel auf godbolt.org zumindest
fehlerfrei übersetzen. Wirklich Sinn ergibt das allerdings nicht, da ja
die eigentliche Methode "ValueText::Draw" leer ist und das "LCD"-Objekt
eine Attrappe.
Roland .. schrieb:> Hallo Wilhelm M.,>> vielen Dank für Deine Antwort.
Gerne!
> Ja, das Element existiert nicht, das ist ganz eindeutig der Fehler,> diese Frage gerade ziemlich peinlich und ich suche nach einer Erklärung.
Alles gut, das pasiert.
> Wie schon vermutet müssen also mehrere Arrays mit jeweils gleiche Typen> gebildet werden. Dasselbe gilt vermutlich auch für Objekte von> abgeleiteten Klassen, die dann ja ebenso eigenständige Typen sind.
Eine Klasse ist eine Klasse ist ein Datentyp. Und C-Arrays sind
typ-homogene Container, also alle Elemente müssen denselben Typ haben.
Ich vermute, dass Du irgendwie Inklusionspolymorphie
(Laufzeitpolymorphie) einsetzen willst, indem Du ein C-Array mit einer
Interface-Klasse erzeugst. Bzw. eigentlich willst Du sie vermeiden,
indem Du mehrere Arrays einsetzt. Das hört sich zunächst etwas
abenteuerlich an. Ich vermute, dass die verschiedenen Typen aber
tatsächlich statisch bestimmbar sind (s.a. Idee: mehrere Arrays). In
diesem Fall kannst Du die Inklusionspolymorphie durch statische
Polymorphie in Form eines additiven Datentyps wie std::variant ersetzen.
Einziges Problem: Du hast keine stdlib++ und musst Dir ein std::variant
selbst schreiben oder aus der stdlib++ kopieren (und etwas anpassen).
Ja, ich habe mir das dann etwa so vorgestellt, dass ich ein Array vom
Typ Zeiger auf die Basisklasse anlege und diesem dann die Referenzen von
den Objekten der abgeleiteten Klassen zuordne. Somit könnte die
Ausgabefunktion aller Objekte elegant mit einer Schleife aufgerufen
werden, wenn die Ausgabefunktion der Basisklasse mit virtual markiert
wird, soweit ich das Konzept der Polymorphie so halbwegs verstanden
habe.
1
for(uint8_ti=0;i<N;i++)valuesPtr[i]->Draw();
Da dies aber wohl einiges an Overhead verursacht, dachte ich zuerst an
mehrere Arrays, je nach Typ, um die Ausgabe zu vereinfachen.
1
ValueNumbernumberValues[N1];
2
ValueStringstringValues[N2];
3
//...
4
for(uint8_ti=0;i<N1;i++)numberValues[i].Draw();
5
for(uint8_ti=0;i<N2;i++)stringValues[i].Draw();
Jedoch lässt sich weder bei der einen, noch bei anderen Variante das
Zuweisen eines neuen, darzustellenden Werts elegant lösen, da die
eigentlichen (Mess-)Werte aus unterschiedlichen Quellen stammen (ADC,
I2C und Zählvariablen) und somit nicht als Array vorliegen. Das Zuweisen
müsste also ohnedies einzeln erfolgen, was bei einem Array dann sogar
komplizierter wird.
1
#define IDX_VOLTAGE 0
2
#define IDX_TIME 1
3
#define IDX_ERROR 0
4
//...
5
ValueNumbernumberValues[N1];
6
ValueStringstringValues[N2];
7
//...
8
numberValues[IDX_VOLTAGE].setValue(voltage);
9
numberValues[IDX_TIME].setValue(timeCnt);
10
stringValues[IDX_ERROR].setValue(errorID);
11
//...
Insofern denke ich mir, in diesem Fall auf ein Array zu verzichten und
dem KISS-Prinzip zu folgen, auch wenn die Ausgabe dann weniger elegant
ausfällt.
Roland .. schrieb:> Ja, ich habe mir das dann etwa so vorgestellt, dass ich ein Array vom> Typ Zeiger auf die Basisklasse anlege und diesem dann die Referenzen von> den Objekten der abgeleiteten Klassen zuordne.
Zeiger in diesem Fall. Zeiger sind eine Form von abstrakten Referenzen.
C++-Referenzen sind non-reseatable.
> Somit könnte die> Ausgabefunktion aller Objekte elegant mit einer Schleife aufgerufen> werden, wenn die Ausgabefunktion der Basisklasse mit virtual markiert> wird, soweit ich das Konzept der Polymorphie so halbwegs verstanden> habe.>>
1
for(uint8_ti=0;i<N;i++)valuesPtr[i]->Draw();
Korrekt.
> Da dies aber wohl einiges an Overhead verursacht, dachte ich zuerst an> mehrere Arrays, je nach Typ, um die Ausgabe zu vereinfachen.
Ja, auf dem AVR ist das speichermäßig nicht optimal, und auch
laufzeitmäßig hast Du einen Nachteil.
Allerdings: spielt das eine Rolle???
>>
1
ValueNumbernumberValues[N1];
2
>ValueStringstringValues[N2];
3
>//...
4
>for(uint8_ti=0;i<N1;i++)numberValues[i].Draw();
5
>for(uint8_ti=0;i<N2;i++)stringValues[i].Draw();
>> Jedoch lässt sich weder bei der einen, noch bei anderen Variante das> Zuweisen eines neuen, darzustellenden Werts elegant lösen, da die> eigentlichen (Mess-)Werte aus unterschiedlichen Quellen stammen (ADC,> I2C und Zählvariablen) und somit nicht als Array vorliegen. Das Zuweisen> müsste also ohnedies einzeln erfolgen, was bei einem Array dann sogar> komplizierter wird.
Von dem Absatz habe ich nichts verstanden.
>
1
#define IDX_VOLTAGE 0
2
>#defineIDX_TIME1
3
>#defineIDX_ERROR0
4
>//...
5
>ValueNumbernumberValues[N1];
6
>ValueStringstringValues[N2];
7
>//...
8
>numberValues[IDX_VOLTAGE].setValue(voltage);
9
>numberValues[IDX_TIME].setValue(timeCnt);
10
>stringValues[IDX_ERROR].setValue(errorID);
11
>//...
Die setter werden doch von den Sensoren aufgerufen, der? Viel anders
wäre das in der anderen Variante doch auch nicht.
> Insofern denke ich mir, in diesem Fall auf ein Array zu verzichten und> dem KISS-Prinzip zu folgen, auch wenn die Ausgabe dann weniger elegant> ausfällt.>>
1
>ValueNumbernumberValueVoltage;
2
>ValueNumbernumberValueTime;
3
>ValueStringstringValueError;
4
>//...
5
>numberValueVoltage.Draw();
6
>numberValueTime.Draw();
7
>stringValueError.Draw();
Auch hier musst Du die Zuordnung der Sensoren haben.
Ich denke weder die Laufzeit noch der zusätzliche benötigte Speicher
spielen eine wesentliche Rolle. Es ist wohl eher nur das Streben nach
Effizienz.
Nun ich meinte einfach, dass am Ende ja immer die Variablen mit den
(Mess-)Werte einzeln über das Aufrufen des Setters dem jeweiligen
„Value“-Objekt zugeordnet werden müssen, der Vorteil eines Arrays hier
also nicht gegeben ist. Anders wäre es, wenn alle (Mess-)Werte bereits
als Array und nicht als Variable vorliegen würden. Dann wäre ein Array
nicht nur bei der Ausgabe besser, sondern auch beim Setzen der neuen
Werte.
Die Setter werden in der Hauptschleife aufgerufen sobald ein neuer Wert
verfügbar ist. Die Werte selbst stammen von einem Sensor, der via
I2C-Bus angebunden ist, vom internen ADC, sowieso von Zählvariablen
(Zeit, Ereignisse). Damit wollte ich zum Ausdruck bringen, dass auf der
Seite der (Mess-)Werte kein Array vorhanden ist, sondern einzelne
Variablen vorliegen. Sicher könnte man aus diesen Variablen ein Array
erstellen und dieses dann wie oben geschildert via Schleife den Settern
übergeben, nur ist das dann auch kein wirklicher Gewinn.
Die Konfiguration des Display entscheidet, an welcher Stelle was
gezeichnet werden soll. D.h. dann, die Value-Objekte existieren. Mit den
Referenzen der Value-Objekte kannst Du dann die Sensoren erzeugen, d.h.
die kennen dann ihr zugehöriges Value-Objekt.
Im Display kannst Du dann einen von den oben vorgestellten Ansätzen
benutzen, um über alle Value-Objekt zu iterieren.
Was Du damit hast, ist im weitesten Sinn ein MVC-Ansatz.
Interessanter Ansatz. Hier wäre also die Datenquelle (I2C, ADC, Zähler)
jeweils ein eigenes Objekt, dem das zugehörige Value-Objekt bekannt ist.
Dieses Quellen-Objekt besitzt dann wohl eine Methode zum messen eines
neuen Werts und leitet diesen dann an das verknüpfte Value-Objekt
weiter?
Das wäre dann gewisser Maßen die vollständige Abstraktion. Das habe ich
so noch nicht in Betracht gezogen. Mein Ansatz war da eher einfacher
Natur: Die Hauptschleife ließt von der jeweiligen Quelle die Daten in
eine lokale Variable und übergibt sie dem passenden Value-Objekt.