C++ Objekt-Instanziierung und Vererbung (AVR C/C++)

Vulpecula

Commander
Registriert
Nov. 2007
Beiträge
2.241
Hallo zusammen!

Ich habe eine Frage bezüglich Objekt-Instanziierung und Vererbung. Das Ganze ist Arduino-spezifisch, aber ich denke, dass der Thread in diesem Sub-Forum am besten aufgehoben ist.

Konkret geht es darum, dass ich eine Klasse erstellt habe, die eine Art "Controller" für ein LC-Display repräsentiert und für meine Bedürfnisse zugeschnittene Funktionen bietet. Innerhalb dieser Klasse wird das Display selbst (also die Schnittstelle zur Hardware) über eine externe Library als Objekt instanziiert. Mein Controller bietet mir nun Funktionen an, mit denen ich Inhalte auf die Displays bringen kann. In der Regel geht es dabei um die Formatierung und Positionierung dessen, was angezeigt wird.

Nun ist es so, dass ich drei Displays habe, die unterschiedliche Daten anzeigen (Klimadaten, Spannungen, ein Timer). Eigentlich müsste jedes dieser Displays eine eigene Controller-Klasse erhalten, da sich ein (nicht kleiner) Teil des Codes von dem der anderen unterscheidet. Aber es gibt nun mal auch gewisse Anteile, die für alle Displays gleich sind.

Hier mal eine etwas gekürzte Version meiner "generischen" Controller-Klasse:

C-ähnlich:
#ifndef GEN_DISP_C
  #define GEN_DISP_C

  #include "Arduino.h"
  #include <Wire.h>
  #include <LiquidCrystal_I2C.h>

  class GenericDisplayController
  {
    public:
      // Konstruktor
      GenericDisplayController(uint8_t displayAddress, uint8_t displayColumns, uint8_t displayRows);

      // öffentliche geteilte Funktionen wie z.B.
      void initialize();

      // öffentliche spezifische Funktionen wie z.B.
      void preloadTimerDisplay();

    private:
      // display Objekt
      LiquidCrystal_I2C _genDisplay;

      // private geteilte und spezifische Funktionen...
      //...
};

#endif /* GEN_DISP_C */
C-ähnlich:
#include "GenericDisplayController.h"
#include <Wire.h>
#include <LiquidCrystal_I2C.h>


// Display Objekt
LiquidCrystal_I2C _genDisplay(uint8_t displayAddress, uint8_t displayColumns, uint8_t displayRows);


// Konstruktor
GenericDisplayController::GenericDisplayController(uint8_t displayAddress, uint8_t displayColumns, uint8_t displayRows) : _genDisplay(displayAddress, displayColumns, displayRows)
{
  //...
}


// öffentliche geteilte Funktionen wie z.B.
void GenericDisplayController::initialize()
{
  _genDisplay.init();
  _genDisplay.backlight();
  _genDisplay.clear();
  return;
}


// öffentliche spezifische Funktionen wie z.B.
void GenericDisplayController::preloadTimerDisplay()
{
    // ...
}

Ich frage mich jetzt, wie ich das Ganze jetzt am besten angehe. Sicherlich könnte ich sämtliche spezifischen Funktionen innerhalb des generischen Controllers implementieren und gut ist. Allerdings hätte ich dann einen riesigen Controller, der wieder sehr unübersichtlich wird.
Eine weitere Idee wäre es, das ganze via Vererbung umzusetzen. Ich erstelle quasi eine generische Klasse, in der schon alle sich überschneidenden Funktionen implementiert sind. Zusätzlich erstelle ich für jede Art Display noch eine weitere Klasse, die von der generischen Klasse erbt und weitere Funktionen implementiert.

Nur: Wie sähe für letzteres die Instanziierung der Display-Objekte aus? Denn das Display-Objekt wird ja in der generischen Klasse erzeugt und dort vom geteilten Code auch "benutzt". Oder ist es so, dass ich einen Pointer auf das Display-Objekt an die Sub-Klassen weiterreiche?

Ich weiß nicht, ob ich mir das ganze ein wenig zu einfach vorstelle, aber vielleicht hat ja jemand ein wenig Input für mich. Vielen Dank! :)

Grüße,
Vulpecula
 
Zuletzt bearbeitet:
Warum erzeugt der GenericDisplayController denn das Display?
Besser ist es, das Display von außen zu erzeugen und dem Controller zu übergeben - einen der Gründe, warum das sinnvoll ist, siehst du ja gerade.

Edit:
Wobei es auch sinnvoll sein kann, alles in einem Controller zu haben. Anhand der Beschreibung bin ich nicht in der Lage zu sagen, was besser ist... meistens sieht man es bei der Implementierung. Dann merkt man meist, was die bessere Option ist.
 
  • Gefällt mir
Reaktionen: Vulpecula
Vulpecula schrieb:
Eine weitere Idee wäre es, das ganze via Vererbung umzusetzen. Ich erstelle quasi eine generische Klasse, in der schon alle sich überschneidenden Funktionen implementiert sind. Zusätzlich erstelle ich für jede Art Display noch eine weitere Klasse, die von der generischen Klasse erbt und weitere Funktionen implementiert.
Zwei weitere Möglichkeiten:
  • Freie Funktionen/statische Funktionen (nimmt als Argument ein Display und initialisiert es)
  • Dependency Injection (das Display wird schon vorinitialisiert dem Konstruktor übergeben)

Das sind häufig schlauere Ideen als über Vererbung zu gehen. Weiterhin kannst du initialize auch schon im Konstruktor aufrufen, bzw. gleich alles in den ctor packen.

Vulpecula schrieb:
Nur: Wie sähe für letzteres die Instantiierung der Display-Objekte aus? Denn das Display-Objekt wird ja in der generischen Klasse erzeugt und dort vom geteilten Code auch "benutzt". Oder ist es so, dass ich einen Pointer auf das Display-Objekt an die Sub-Klassen weiterreiche?
Du musst die Instantiierung durchführen, wo du den DisplayTyp weißt. Im geteilten Code können dann auch nur geteilte Funktionen aufgerufen werden:

C++:
#include <memory>

class GenericDisplayController
  {
    public:
      // Konstruktor
      GenericDisplayController(uint8_t displayAddress, uint8_t displayColumns, uint8_t displayRows);

      // öffentliche geteilte Funktionen wie z.B.
      void initialize();
};

class SpecificDisplayController: public GenericDisplayController{
    public:

    SpecificDisplayController(uint8_t displayAddress, uint8_t displayColumns, uint8_t displayRows);

    void preloadTimerDisplay();

};


int main(){
    auto displayController = std::make_unique<SpecificDisplayController>(1,1,1);
  
    //here: aware of specific controller
    displayController->initialize();
    displayController->preloadTimerDisplay();
 
    std::unique_ptr<GenericDisplayController> genericDisplayController = std::move(displayController);
    //now: pass to entity that is unaware of the specific controller

    //here: unaware of specific controller
    genericDisplay->initialize();
    // genericDisplay->preloadTimerDisplay(); ERROR: not a specific display controller anymore
}
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Vulpecula
Hey und danke schonmal für die Antworten!

Wenn ich so darüber nachdenke, dann wäre es eigentlich nicht ganz so tragisch, wenn ich nur einen Display-Controller schreibe und auch spezifische Methoden mit aufnehme. Hauptziel ist es erstmal, dass der Spaghetti-Code aus der Hauptklasse verschwindet. Eigentlich. Tatsächlich kann ich mir hier noch ein wenig Wissen aneignen, deswegen möchte ich mal nicht den faulen Weg gehen. ;)

@new Account()
make_unique ist C++14, oder? Ich bin mir gerade nicht sicher, aber ich vermute, dass der AVR Compiler da aussteigen wird. Leider bin ich da etwas eingeschränkt. Ich muss das aber mal ausprobieren, sobald ich etwas mehr Zeit habe.
Bezüglich der beiden weiteren Möglichkeiten: Beide Möglichkeiten würden aber wieder bedeuten, dass jeder Controller den gleichen, geteilten Code enthält, oder verstehe ich das falsch?

@tollertyp
Die Idee dahinter ist, den bisherigen Spaghetti-Code des Display-Teils aus der Hauptklasse der Firmware herauszunehmen. Am Ende soll sich der Controller um den Part der Anzeige kümmern; ich will ihm nur noch sagen können "Hier ist ein Datum von Typ Luftfeuchtigkeit, bitte anzeigen bzw. aktualisieren". Das Objekt vom Typ "Klimadaten-Display-Controller" nimmt das Datum in einer spezifischen Funktion an, setzt den Cursor an die richtige Position und gibt es dort aus (grob vereinfacht).
 
Also es gibt genug Möglichkeiten...

Du könntest:
a) alles in eine Klasse
b) Vererbung
c) Komposition
machen

Komposition machst du ja schon mit dem Display, das vom Controller genutzt wird. Alternativ könntest du auch einen allgemeinen Controller machen, und statt von ihm zu erben nutzen 3 spezielle Controller den allgemeinen - so wie der allgemeine Controller das Display nutzt.

Also bei Komposition würden die drei einzelnen Controller nur wissen, wie sie den allgemeinen Controller ansteuern.

Was ich in deinem Fall konkret empfehlen würde, das kann ich wie gesagt nicht beurteilen ohne es konkret zu sehen.
 
Vulpecula schrieb:
make_unique ist C++14, oder? Ich bin mir gerade nicht sicher, aber ich vermute, dass der AVR Compiler da aussteigen wird.
Ja, kannst aber einfach nachrüsten: https://stackoverflow.com/questions/17902405/how-to-implement-make-unique-function-in-c11

Ansonsten klappt natürlich genauso das verpönte new.


Vulpecula schrieb:
Beide Möglichkeiten würden aber wieder bedeuten, dass jeder Controller den gleichen, geteilten Code enthält, oder verstehe ich das falsch?
Nein:
1) funktion irgendwo separat ablegen und dann in allen spezifischen controller verwenden
2) da kannst du auch eine funktion erstellen und nach dem erstellen des displays nutzen (bevor das display an den controller übergeben wird)

In beiden Fällen hast du die Funktion nur einmal.

@tollertyp willst du aufs decorator pattern raus?
 
Muss ich auf ein Pattern hinaus wollen?
Ich spreche eher von Composition over Inheritance.
 
tollertyp schrieb:
Alternativ könntest du auch einen allgemeinen Controller machen, und statt von ihm zu erben nutzen 3 spezielle Controller den allgemeinen - so wie der allgemeine Controller das Display nutzt.

Also bei Komposition würden die drei einzelnen Controller nur wissen, wie sie den allgemeinen Controller ansteuern.
Ne, aber du scheinst genau das zu beschreiben und ist ja nur eine Möglichkeit von vielen.
 
Ich denke nicht, dass ich das mache.
Denn die speziellen Controller würden ja nicht die Schnittstelle des allgemeinen Display-Controllers anbieten - dafür gäbe es ja gar keinen Grund.
 
new Account() schrieb:
1) funktion irgendwo separat ablegen und dann in allen spezifischen controller verwenden

Ergo sowas wie eine Utility-Klasse, von der keine Instanz erzeugt wird, sondern die innerhalb des jeweiligen Controllers eingebunden wird und gewisse Funktionen bietet? (Natürlich muss der jeweiligen Funktion dann eine Referenz des Displays übergeben werden.)

Hmm... it‘s tempting. Wäre zumindest eine simple Art, um das„Problem“ zu lösen. Je weniger doppelten Code ich habe, umso besser.

Beim geteilten Code handelt es sich übrigens nicht nur um das Initialisieren, da gibt es noch mehr. Hab es nur als Beispiel benutzt.
 
Vulpecula schrieb:
Ergo sowas wie eine Utility-Klasse, von der keine Instanz erzeugt wird, sondern die innerhalb des jeweiligen Controllers eingebunden wird und gewisse Funktionen bietet? (Natürlich muss der jeweiligen Funktion dann eine Referenz des Displays übergeben werden.)
Da brauchst du nicht zwingend eine Klasse dafür anlegen. Ob eine Klasse Sinn ergibt musst du anhand der konkreten Situation entscheiden.
Für eine kleine Funktion sehe ich keinen Sinn hinter einer Klasse.

Merke: Eine Klasse erfüllt möglichst einen einzigen Zweck.

Vulpecula schrieb:
Je weniger doppelten Code ich habe, umso besser.
Definitiv.


Wenn es nur Initialisierung wäre, würde ich aber stark dazu tendieren Dependency Injection nutzen, d.h. Display klasse erst initialisieren und dann dem Controller übergeben.
(Dann kannst du die Funktion auch dahin packen wo die Instanzen der Klassen erstellt werden)
 
new Account() schrieb:
Wenn es nur Initialisierung wäre, würde ich aber stark dazu tendieren Dependency Injection nutzen, d.h. Display klasse erst initialisieren und dann dem Controller übergeben.
(Dann kannst du die Funktion auch dahin packen wo die Instanzen der Klassen erstellt werden)

Das wäre dann die ‚Hauptklasse‘, wo noch alles liegt. 😅 Genau da sollte es ja raus. Momentan ist es eine große Datei. Leider ist das ganze Projekt „historisch gewachsen“ und niemand hat es für nötig empfunden, das mal etwas sauberer zu machen. Im Zuge einer Hardware Umrüstung habe ich mich dem jetzt mal angenommen. Am liebsten hätte ich ein möglichst geringes Coupling, da ich nicht vorhersehen kann, wann mal wieder jemand auf die Idee kommt, das System umzukrempeln.

Edit: Sobald ich zu Hause bin werd ich mal einen Ausschnitt aus dem aktuellen Code hochladen, damit man sieht, wie es bisher abläuft.
 
Vulpecula schrieb:
. Am liebsten hätte ich ein möglichst geringes Coupling, da ich nicht vorhersehen kann, wann mal wieder jemand auf die Idee kommt, das System umzukrempeln.
Alleine von den Infos, die ich habe, wäre es weniger coupling (und mehr cohesion), wenn nicht jede einzelne Klasse die Initialisierung aufrufen würde, sondern es beim Aufrufer gesammelt machen würde.

Rausziehen gern, aber schon mit Sinn und Verstand. Und sollte normalerweise auch das Refactoring nicht behindern.
Ist aber viel Spekulation ohne mehr Details.

EDIT: Den edit habe ich noch nicht gesehen als ich meinen Beitrag geschrieben habe
 
Wie gesagt, ich muss erst zu Hause sein. Vom Telefon aus ist das ein ziemlicher Krampf 😅
 
Vulpecula schrieb:
make_unique ist C++14, oder? Ich bin mir gerade nicht sicher, aber ich vermute, dass der AVR Compiler da aussteigen wird.
Ja und nein. Ein aktueller avr-gcc unterstützt sogar alle C++17 language features. Aber, es gibt keine C++ standard library für avr-gcc, das heißt alles was mit std:: anfängt gibt es nicht. Damit fallen auch alle smart pointer weg, was aber sowieso gut ist, denn wir reden hier von einem Microcontroller ohne OS, da ist dynamic memory sowieso eine sehr schlechte Idee. Falls du also in deinem Code ein new oder malloc hast, hast du schon mal einen sehr starken code-smell um den du dich kümmern solltest.

Welche Abstraktion in deinem Fall die Beste ist, würde ich sagen ist sehr subjektiv und alles hat Vor- und Nachteile. Du solltest aber möglichst Abstraktionen verwenden die auf einem Microcontroller Sinn machen. Also zB virtual inheritance würde ich auf gar keinen Fall verwenden. Außerdem so viel wie möglich zur compile-time machen, damit zur runtime möglichst wenig schief gehen kann.

Gruß
BlackMark
 
  • Gefällt mir
Reaktionen: Vulpecula und new Account()
@BlackMark
Ja, das ist nachvollziehbar. Was in meinem konkreten Fall aber das Beste™ wäre ist vermutlich auch eine Sache der Erfahrung, die ich immer noch Sammle. Gerade jetzt fände ich es gut, wenn man Klassen (bzw. das, was ich "Controller" genannt habe) einfach aus dem System herauslösen könnte, sofern dies nötig ist. Soll heißen: Wenn jemand auf die Idee kommt, statt den HD44780er ganz andere Displays zu verbauen, dann will ich in der Lage sein, nur die entsprechenden (Controller-)Klassen anpassen zu müssen. Meine Hauptklasse ruft dann weiterhin immer noch die selben Methoden gleichnamige Methoden mit der selben Signatur auf.

Ich bin mir immer noch unsicher, von welcher Seite ich es jetzt aufziehen soll. Wenn ich nur eine Controller-Klasse baue in der ich alles unterbringe, habe ich zwar keinen doppelten Code, aber wieder eine sehr aufgeblähte Klasse. Dazu kommt, dass ich dann Vorkehrungen treffen muss, damit z.B. auf dem Display für den Timer plötzlich keine Temperaturen angezeigt werden. Zusätzlich hätte ich jede Menge "toten Code", da ein Controller, der sich z.B. um die Spannungsanzeige kümmert, den Code für den Timer nie anfassen wird. Dem gegenüber stünde dann die Lösung mit drei Klassen - für jedes Display ein eigener Controller. Damit hätte ich zwar die obigen Probleme nicht, aber dafür wieder doppelten Code.

Nungut... Ich werde noch ein wenig darüber sinnieren, wie ich es jetzt aufziehe. Vielen Dank erstmal für Euren Input! :bussi:
 
Ich kann dir sagen wie ich es machen würde, was aber nicht bedeutet, dass das the best solution™ ist.

Ich schreibe generische Interfaces für AVR als template Klassen, die die eigentliche Implementierung als template Parameter entgegennehmen. Zum Beispiel, das ist mein generisches i2c Interface. Die eigentliche Implementierung ist dann Driver und kann zum Beispiel eine i2c Implementierung in Software sein, oder die Hardware von einem bestimmten Chip. Verwenden würde man das ganze dann so.

Dieses Pattern könntest du auch für dein Problem verwenden. Dein Controller nimmt dann die eigentliche Implementierung der Hardware als template Parameter entgegen. Du musst natürlich sicherstellen, dass das Interface zusammen passt und im schlimmsten Fall musst du dir eben einen Interface translation layer dazwischen bauen. Geht in C++ ja prinzipiell komplett ohne runtime overhead.

Gruß
BlackMark
 
  • Gefällt mir
Reaktionen: Vulpecula
Macht auf jeden Fall Sinn, wenn compiletime Polymorphisms möglich ist (ansonsten könnte man auch auf std::variant ausweichen, via alternativer (std) Implementierung).
Das geht aber das Problem des TEs nicht wirklich an: codeduplizierung.

Aber: beide Vorschläge ohne Vererbung lassen sich damit kombinieren.
Welche Variante von diesen sinnvoller ist, hängt wie gesagt von der konkreten Konstellation ab. Wäre es nur das initialize: definitiv dependency injection, andernfalls ggf. eine separate klasse/funktionen
 
  • Gefällt mir
Reaktionen: Vulpecula
@new Account() @BlackMark

Ich denke, irgendeinen Tod werde ich sterben müssen. Die Idee der generischen Interfaces gefällt mir schonmal sehr. Ich wünschte ich hätte gerade nur mehr Zeit um mal etwas mehr Code zu produzieren.

new Account() schrieb:
Wäre es nur das initialize: definitiv dependency injection, andernfalls ggf. eine separate klasse/funktionen
Ich werde, sobald ich es zeitlich einrichten kann, mal genau schauen, was ich an shared Code so habe. Ich habe so die Vorahnung, dass es weniger sein könnte, als gedacht. Viel mehr als das Init und eher generelle Funktionen wie clear() etc. sind es vermutlich nicht.
 
new Account() schrieb:
Das geht aber das Problem des TEs nicht wirklich an: codeduplizierung.
Warum nicht? Generischer Code, der für alle Displays gleich ist, kommt in den Controller. Und alle Display spezifischen Sachen sind dann Teil der eigentlichen Hardware Implementierung. Dann sollte es eigentlich keine Code duplication geben. Außer natürlich, dass der Controller die Funktionen der eigentlichen Implementierung callen muss. Das ist aber Syntax overhead und nicht Code duplication, würde ich sagen.
Je nachdem wie dann wirklich die konkrete Konstellation ist, macht es vielleicht Sinn noch einen zusätzlichen Layer an Abstraktion einzuziehen, oder Sachen auszulagern. Ist eben alles sehr abhängig von den Anforderungen/der Situation.

@Vulpecula Es ist oft hilfreich in so einer Situation einen Top-down statt einen Bottom-up Ansatz zu wählen. Du kannst ja einfach mal hinschreiben wie du deinen Controller gerne verwenden würdest und dir dann überlegen, ob man das implementieren kann.

Gruß
BlackMark
 
  • Gefällt mir
Reaktionen: Vulpecula
Zurück
Oben