Objektinitialisierung mit Parametern im Konstruktor in C++

OP #7459533
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.

OP #7459592
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.

#7459670
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

#7459696
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
{ }
#7459719
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<>.

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

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

#7459834
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

OP #7460231
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.

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

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

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

OP #7460600
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.

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

OP #7461166
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.

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

OP #7461213
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.

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

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

OP #7462983
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?

#7463162
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
};
#7463177
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.

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

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

#7463196
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
};
OP #7463845
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.

OP #7464489
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.

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

OP #7464588
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.

OP #7464714
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.

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

OP #7465368
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();
#7465521
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.

OP #7465889
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.

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

OP #7466600
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.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren