Forum: Compiler & IDEs Objektinitialisierung mit Parametern im Konstruktor in C++


von Roland .. (rowland)


Lesenswert?

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.
1
class ObjectA
2
{
3
  private:
4
    const uint8_t ValueA;
5
  
6
  public:
7
    ObjectA(const uint8_t ValueA);
8
}
9
10
ObjectA::ObjectA(const uint8_t ValueA) : ValueA(ValueA) { }
11
12
class ObjectB
13
{
14
  private:
15
    ObjectA Array[2];
16
  
17
  public:
18
    ObjectB(const uint8_t ValueB);
19
}
20
21
ObjectB::ObjectB(const uint8_t ValueB)
22
{
23
  Array = { {ValueB}, {ValueB + 1} };
24
}

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".
1
class ObjectA
2
{
3
  private:
4
    const uint8_t ValueA;
5
  
6
  public:
7
    ObjectA();
8
    void Init(const uint8_t ValueA);
9
}
10
11
ObjectA::ObjectA() { }
12
ObjectA::Init(const uint8_t ValueA) { ObjectA::ValueA = ValueA; }
13
14
class ObjectB
15
{
16
  private:
17
    ObjectA Array[3];
18
  
19
  public:
20
    ObjectB(const uint8_t ValueB);
21
}
22
23
ObjectB::ObjectB(const uint8_t ValueB)
24
{
25
  Array[0].Init(ValueB);
26
  Array[1].Init(ValueB + 1);
27
}

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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?

von Roland .. (rowland)


Lesenswert?

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
ObjectA Array[] = { { 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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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?

von Frank (fritzi)


Lesenswert?

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(const uint8_t ValueB)
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

von Rolf M. (rmagnus)


Lesenswert?

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):
1
ObjectB::ObjectB(const uint8_t ValueB)
2
    : Array{ {ValueB}, { static_cast<uint8_t>(ValueB + 1) } }
3
{ }

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

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:
1
    ObjectB(const uint8_t ValueB) : Array{ ValueB, ValueB + 1} {}

Falls man den nicht will:
1
class ObjectA {
2
    const uint8_t ValueA;
3
  public:
4
    explicit ObjectA(const uint8_t ValueA) : ValueA{ValueA} { }
5
};
6
7
class ObjectB {
8
    ObjectA Array[2];
9
  public:
10
    ObjectB(const uint8_t ValueB) : Array{ObjectA(ValueB), ObjectA(ValueB + 1)} {}
11
};

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<>.

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Falk S. (db8fs)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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
ClassA x { 3 };
dann finde ich das nicht so ganz passend, denn ich erzeuge damit ja 
keine Klasse, sondern ein Objekt.

von Oliver S. (oliverso)


Lesenswert?

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

von Roland .. (rowland)


Lesenswert?

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
class ObjectB
2
{
3
  private:
4
    ObjectA Array[] = { { 0 }, { 1 } };
5
  public:
6
    ObjectB(const uint8_t ValueB);
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
LCD Lcd;
2
LCD LCD1;
3
LCD _LCD;
4
LCD LCDObject;
5
_LCD LCD;
6
LCDClass LCD;

Am ehesten sagt mir noch die letzte Variante zu, auch wenn durch den 
Anhang "Class" der Klassename teilweise lang wird.

Beste Grüße,
Roland.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

von Falk S. (falk_s831)


Lesenswert?

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).

von Roland .. (rowland)


Lesenswert?

@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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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
using terminal = Uart<0, Position<Port<B>>>;
2
using gps = 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.

von Roland .. (rowland)


Lesenswert?

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.

von Falk S. (db8fs)


Lesenswert?

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:
1
class Car
2
{
3
  struct Wheel
4
  {
5
  };
6
7
  Wheel m_frontLeft;
8
  Wheel m_frontRight;
9
  Wheel m_rearLeft;
10
  Wheel m_rearRight;
11
12
  public:
13
    Car() : m_frontLeft(), m_frontRight(), m_rearLeft(), m_rearRight() {}
14
};

Instanzen/Objekte können dann sehr konkret sein, für z.B. eine Flotte 
von 3 Fahrzeugen:
1
 Car vwPassatBlau;
2
 Car miniRot;
3
 Car kfz_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
Car vwKaefer;
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.

von Roland .. (rowland)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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" ...

von Roland .. (rowland)


Lesenswert?

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.
1
#define SETBIT(Reg, Bit)   Reg |= (1 << Bit)
2
#define BITISSET(Reg, Bit) (Reg & (1 << Bit))
3
4
class Uart
5
{
6
  public:
7
    static void enable(const uint16_t boudRateRegisterValue);
8
    static void printChar(const uint8_t c);
9
  private:
10
    Uart();
11
    Uart(const Uart&);
12
    Uart& operator=(const Uart&);
13
};
14
15
void Uart::enable(const uint16_t baudRateRegisterValue)
16
{
17
  SETBIT(UCSRB, TXEN);
18
  SETBIT(UCSRB, RXEN);
19
  UBRRL = baudRateRegisterValue;
20
}
21
22
void Uart::printChar(const uint8_t c)
23
{
24
  while (!BITISSET(UCSRA, UDRE));
25
  UDR = c;
26
}
27
28
Uart::enable(1);
29
Uart::printChar('H');

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?

von A. B. (Firma: ab) (bus_eng)


Lesenswert?

1
int main() {
2
  using uart = Uart;
3
  
4
  uart::enable(1);
5
  uart::printChar('H');
6
};

mit -mmcu=atmega16
https://godbolt.org/z/PMGznh9EP

von Wilhelm M. (wimalopaan)


Lesenswert?

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:
1
template<auto N>
2
struct Uart {
3
   // ...
4
};

oder auch ggf. für meherer µC:
1
template<auto N, typename MCU>
2
struct Uart {
3
   // ...
4
};

von J. S. (jojos)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Vincent H. (vinci)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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
1
template<auto N, typename MCU>
2
struct Uart;
3
4
template<auto N, STM32Gxx MCU>
5
struct Uart<N, MCU> {
6
   // ...
7
};
8
9
template<auto N, AVR_DA MCU>
10
struct Uart<N, MCU> {
11
   // ...
12
};

von Roland .. (rowland)


Lesenswert?

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.

von Roland .. (rowland)


Lesenswert?

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:
1
class ValueText
2
{
3
  private:
4
    const uint8_t position;
5
    const char *text;
6
    
7
    static LCD *lcd;
8
  
9
  public:
10
    ValueText(const uint8_t position, const char *text);
11
    void Draw(void);
12
    
13
    static void ConfigStatic(LCD *lcd);
14
};
15
16
ValueText::ValueText(const uint8_t position, const char *text) : position(position), text(text) { }
17
void ValueText::Draw(void) { /* Textausgabe an 'position' */ }
18
19
LCD *ValueText::lcd;
20
void ValueText::ConfigStatic(LCD *lcd)
21
{
22
  ValueText::lcd = lcd;
23
}
24
25
26
class Info
27
{
28
  private:
29
    static ValueText value1;
30
    static ValueText values[];
31
  
32
  public:
33
    Info() = delete;
34
    ~Info() = delete;
35
    Info( const Info &c ) = delete;
36
    Info& operator=( const Info &c ) = delete;
37
  
38
    static void Config(LCD *lcd, uint8_t parameter);
39
    static void Draw(void);
40
};
41
42
static const char text1[] PROGMEM = "Text 1";
43
static const char text2[] PROGMEM = "Text 2";
44
static const char text3[] PROGMEM = "Text 3";
45
46
ValueText Info::value1(10, text1);
47
ValueText Info::values[2] =
48
{
49
  { 20, text2 },
50
  { 30, text3 },
51
};
52
53
void Info::Config(LCD *lcd, uint8_t parameter)
54
{
55
  ValueText::ConfigStatic(lcd);
56
}
57
58
void Info::Draw(void)
59
{
60
  value1.Draw(); // "Text 1" wird ausgegeben
61
  for(uint8_t i = 0; i < 2; i++) values[2].Draw(); // Ausgabe scheitert
62
}
63
64
65
Info::Config(&Lcd1, 0);
66
Info::Draw();

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

Roland .. schrieb:
> for(uint8_t i = 0; i < 2; i++) values[2].Draw(); // Ausgabe scheitert

Du greifst auf ein nicht existierendes Element zu.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

: Bearbeitet durch User
von Roland .. (rowland)


Lesenswert?

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.

von A. B. (Firma: ab) (bus_eng)


Lesenswert?

@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

: Bearbeitet durch User
von Roland .. (rowland)


Lesenswert?

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
int main(void)
2
{
3
    LCD Lcd1;
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.

von A. B. (Firma: ab) (bus_eng)


Lesenswert?

Etwas anders hingeschrieben, comiliert, rein akademisch ...

https://godbolt.org/z/fTr888579

von Wilhelm M. (wimalopaan)


Lesenswert?

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).

von Roland .. (rowland)


Lesenswert?

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_t i = 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
ValueNumber numberValues[N1];
2
ValueString stringValues[N2];
3
//...
4
for (uint8_t i = 0; i < N1; i++) numberValues[i].Draw();
5
for (uint8_t i = 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
ValueNumber numberValues[N1];
6
ValueString stringValues[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.
1
ValueNumber numberValueVoltage;
2
ValueNumber numberValueTime;
3
ValueString stringValueError;
4
//...
5
numberValueVoltage.Draw();
6
numberValueTime.Draw();
7
stringValueError.Draw();

von Wilhelm M. (wimalopaan)


Lesenswert?

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_t i = 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
ValueNumber numberValues[N1];
2
> ValueString stringValues[N2];
3
> //...
4
> for (uint8_t i = 0; i < N1; i++) numberValues[i].Draw();
5
> for (uint8_t i = 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
> #define IDX_TIME 1
3
> #define IDX_ERROR 0
4
> //...
5
> ValueNumber numberValues[N1];
6
> ValueString stringValues[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
> ValueNumber numberValueVoltage;
2
> ValueNumber numberValueTime;
3
> ValueString stringValueError;
4
> //...
5
> numberValueVoltage.Draw();
6
> numberValueTime.Draw();
7
> stringValueError.Draw();

Auch hier musst Du die Zuordnung der Sensoren haben.

von Roland .. (rowland)


Lesenswert?

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.
1
for (uint8_t i = 0; i < N; i++) valuesPtr[i]->SetValue(sensorValues[i]);
2
//...
3
for (uint8_t i = 0; i < N; i++) valuesPtr[i]->Draw();

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Roland .. (rowland)


Lesenswert?

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.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.