Polymorphismus vs. Switch-Case vs. Funktionspointer

Hallo zusammen,

ich hab einen Streifen WS2812 LEDs auf dem ich verschiedene Animationen (ca. 10-15 verschiedene) anzeigen möchte. Die Animationen sollen dynamisch während der Laufzeit ausgetauscht werden können. Dabei habe ich ein Hauptprogramm, das für die Auswahl der entsprechenden Animation und das Timing zuständig ist, die einzelnen Animationen bestehen dann eigentlich nur aus dem Aufruf "nextStep()". Meine Frage ist nun, wie das Ganze am besten architektonisch umgesetzt werden kann.
Das Lehrbuch sagt hier, ganz klarer Fall für Polymorphismus: Abstrakte Oberklassse "Animation" von der die einzelnen Animation erben und jeweils die Methode "nextStep()" implementieren. Im Hauptprogramm wird dann das Obekt der Animation jeweils dynamisch ausgetauscht. Dadurch wird der Code elegant, übersichtlich und es können sehr leicht neue Animationen hinzugefügt werden.
Mein Bedenken ist allerdings, das die 2k RAM des atmega328 für die dynamische Speicherverwaltung nicht ausreichen.
Die Alternative die ich sehe, wäre ja einfach für jede Animation jeweils eine eigene Funktion und eine Switch-Anweisung im Haptprogramm, die dann die entsprechende Funktion der aktuellen Animation auswählt.
Man kann natürlich auch mit Funkltionspointern tricksen, und ein "Array aus Funktionen" machen und dann über den Zeiger die entsprechende Funktion aufrufen.

Das Ganze ist ja eher eine Grundsatzfrage, daher wollte ich bevor ich mit dem eigentlichen Programmieren anfange erstmal nach euren Meinungen fragen.

Gruß,
Marv

Der Speicher wird dafür ausreichen. Ob es eine effiziente Nutzung des Speichers ist, ist eine andere Frage. Wenn du dir das Freigegeben des Speichers sparen willst, könntest du vielleicht von jeder Animation ein Objekt anlegen, egal ob du es verwendest, oder nicht. Das braucht natürlich erst mal mehr RAM, aber man kann dann einfach zwischen den Animationen wechseln ohne Objekte zu löschen und neu zu erzeugen.

Funktions-Zeiger und Arrays aus Funktions-Zeigern ist die C Variante, da es in C keine Klassen gibt (das kam eben erst mit C++), aber es klappt sehr schön. Wenn man noch andere Parameter mit angeben möchte, z.B. die Dauer der Animation oder wie oft sie wiederholt werden soll, kann man das mit einem Funktionszeiger in ein struct verpacken.

Wäre auszuprobieren, ob und wieviel RAM die virtuellen Funktionen kosten. Intern wird das auch über Funktionszeiger realisiert.
Mit dynamischer Speicherverwaltung hat das nichts zu tun. Deine 10 - 15 Klassen mit je einer Instanz oder 10-15 Objekte einer Klasse mit jeweils verschiedener callback Funktion können in beiden Fällen statisch angelegt sein.

Wenn die Animationen als Datensätze implementiert werden, reicht ein einziger Interpreter. Wenn so ein Datensatz ins RAM paßt, kann er auch vom internen/externen EEPROM, Flash, SD o.ä. geladen werden. Flash oder EEPROM kann ggf. auch ohne Laden ins RAM benutzt werden, sollte für Animationen immer noch schnell genug sein.

Die Animation besteht nicht nur aus reinen Daten, sondern auch aus Code, d.h. eine Funktion. Und die muss man irgendwie allgemein ansprechen können. Und da hat man dann eben die Wahl zwischen Klassen und Polymorphie und Funktionszeiger.

Aber wie gesagt muss man Klassen nicht dynamisch ansprechen. Dass geht genauso statisch.

Vielen für die Ideen, da ist ja schon einiges interessante dabei :slight_smile:

Serenifly:
Funktions-Zeiger und Arrays aus Funktions-Zeigern ist die C Variante, da es in C keine Klassen gibt (das kam eben erst mit C++), aber es klappt sehr schön. Wenn man noch andere Parameter mit angeben möchte, z.B. die Dauer der Animation oder wie oft sie wiederholt werden soll, kann man das mit einem Funktionszeiger in ein struct verpacken.

Gefällt mir! Allerdings ist mir nicht klar, wie die Instanzen erstellt werden. Diese Methode ist im Prinzip das gleiche wie mit Polymorphismus, allerdings sind hier die Animationen Objekte desselben Datentyps statt je einem eigenen speziellen, hab ich das richtig verstanden?

DrDiettrich:
Wenn die Animationen als Datensätze implementiert werden, reicht ein einziger Interpreter.

Serenifly:
Die Animation besteht nicht nur aus reinen Daten, sondern auch aus Code, d.h. eine Funktion. Und die muss man irgendwie allgemein ansprechen können.

Diese Idee finde ich fast noch interessanter. Dann speichere ich die Animationen als kleine "Programme", die dann von meinem Hauptprogramm interpretiert werden. Zwar können die Animationen dann nicht mehr ganz so flexibel sein wie in den anderen Lösungen, jedoch leicht neue erstellt werden, evtl sogar mit einem spezielle dafür geschriebenen Entwicklungstool auf dem PC.
Ist aber wahrscheinlich overkill, da man erst mal eine eigene Interpretersprache basteln müsste.

Serenifly:
Aber wie gesagt muss man Klassen nicht dynamisch ansprechen. Dass geht genauso statisch.

Ehrlich gesagt kenne ich OOP in C++ nur die Grundlagen. An der Uni machen wir das nur in Java, und da ist was OOP betrifft, eh alles anders.

Ich denke, ein reines Array aus Funktionspointern auf Funktionen (die dann entsprechend statische Variablen besitzen um den aktuellen Zustand zu speichern) ist für den Anfang kein schlechter Ansatz.

MGOS:
Diese Idee finde ich fast noch interessanter. Dann speichere ich die Animationen als kleine "Programme", die dann von meinem Hauptprogramm interpretiert werden.

Huh? Wieso bist du denn auf Polymorphie und Funktionszeiger gekommen wenn du nicht schon vor hattest die Logik in Funktionen, bzw. Methoden zu verpacken? Der ganze Sinn von Polymorphie hier ist es eine Methode einer Unterklasse über einen Zeiger auf ein Objekt der Oberklasse aufzurufen.

Wenn es nur darum geht ein paar Variablen pro Animation zu verwalten - aber keinen separaten Code für jede Funktion zu haben - tut es auch ein struct, bzw. Arrays aus structs

Serenifly:
Huh? Wieso bist du denn auf Polymorphie und Funktionszeiger gekommen wenn du nicht schon vor hattest die Logik in Funktionen, bzw. Methoden zu verpacken? Der ganze Sinn von Polymorphie hier ist es eine Methode einer Unterklasse über einen Zeiger auf ein Objekt der Oberklasse aufzurufen.

Prinzipiell ist das Konzept ähnlich: Bei der Idee mit den Funktionszeigern/Polymorphismus ist die Animation halt C-Code, der direkt zu was auf dem ATmega lauffähigen kompiliert wird.
In der Idee von DrDiettrich wäre das ein speziell für die Animationen entwickelter Bytecode, der vom Hauptprogramm auf dem ATmega interpretiert wird. Dabei liegen die Animationen eher als Daten als als Maschinencode vor. Indirekt sind das natürlich wieder Funktionszeiger. Hat natürlich einen sehr sehr sehr viel höheren Entwicklungsaufwand, aber später wird es einfach möglich sein, Animationen mittels externen Datenträgern oder gar über Bluetooth auszutauschen.

Ehrlich gesagt kenne ich OOP in C++ nur die Grundlagen. An der Uni machen wir das nur in Java, und da ist was OOP betrifft, eh alles anders.

Gerade Arrays sind in Java was ganz anderes.
Wenn du kein Java kennen würdest, würdest du C-Arrays für viel einfacher halten, wenn sie dir später in java über den Weg laufen.
In C++ (und besonders auf dem Arduino) kommst du ohne new aus.

Ah, du hattest dich auf das von DrDiettrich bezogen. Hat mich etwas verwirrt weil du mich dabei zitiert hast :slight_smile:

Und was Java und C++ betrifft. Die Grundlagen von OOPs sind überall gleich. Was du in Java machen kannst, kannst du auch in C++ machen. Der größte Unterschied ist wahrscheinlich, dass in Java alle Methoden automatisch virtuell sind, während man sie in C++ explizit virtuell deklarieren muss.
Und gerade an der Uni sollte man das allgemein lernen und dann in der Lage sein diese Prinzipien auch auf andere Sprachen anzuwenden wenn man die Syntax kann.

Was dynamische vs. statische Daten betrifft, hast du das auch auf dem Arduino ohne da groß in OOP einzusteigen. z.B. wenn du sowas machst:

Servo myServo;

oder:

Servo myServos[2];

Da werden auch Objekte erzeugt. Ganz ohne new. Die existieren dann solange sie Scope haben, d.h. wenn sie global sind immer. So ähnlich kannst du das auch mit Objekten machen die deine Animationen enthalten. So in der Art (Standard C++, daher cout):

class BaseClass
{
public: 
  virtual void run() = 0;
};

class MyClass1 : public BaseClass
{
public:
  void run(){
    cout << "Klasse 1" << endl;
  }
};

class MyClass2 : public BaseClass
{
public:
  void run(){
    cout << "Klasse 2" << endl;
  }
};

und dann:

MyClass1 obj1;
MyClass2 obj2;
BaseClass* objects[] = { &obj1, &obj2 };

for (int i = 0; i < sizeof(objects) / sizeof(objects[0]); i++)
   objects[i]->run();

Du kannst aber auch einmal new machen und dann nie delete. Dann hast du auch keine Probleme mit der Performance. Dynamischer Speicher ist nur aufwendig wenn er auch wirklich dynamisch verwenden wird.

Du kannst aber auch einmal new machen und dann nie delete. Dann hast du auch keine Probleme mit der Performance. Dynamischer Speicher ist nur aufwendig wenn er auch wirklich dynamisch verwenden wird.

Per new erzeugte Objekte werden nicht von der automatischen Speicherverbrauchsanzeige der IDE mit eingerechnet. Sie ist dann also ungenau, und damit problematisch.
Es wird auch für jeden dynamisch erzeugten Speicherbereich ein MemoryControlBlock angelegt.
Also Verwaltungsaufwand und Speicherverplemperung.

Von "new" sollte man bei den keinen AVR Zwergen absehen.

MGOS:
In der Idee von DrDiettrich wäre das ein speziell für die Animationen entwickelter Bytecode, der vom Hauptprogramm auf dem ATmega interpretiert wird.

Ich dachte an einfache arrays mit Bitmustern (structs), die einfach nacheinander (mit Pausen dazwischen) ausgegeben werden. Was könnte/sollte in einer Animation sonst noch variabel sein?

@ Serenifly & Combie : Danke, das hift mir weiter. :slight_smile:

DrDiettrich:
Ich dachte an einfache arrays mit Bitmustern (structs), die einfach nacheinander (mit Pausen dazwischen) ausgegeben werden. Was könnte/sollte in einer Animation sonst noch variabel sein?

Zum Beispiel die Farbe. Es gibt z.B. Animationen, die einfarbig sind, andere sind Farbverläufe, oder wechseln die Farben. Natürlich kann man dann alle LEDs mit 24 bit speichern. Bei einem Lauflicht über 32 LEDs hin und zurück sind das 64 Frames à 32 LED à 3 byte = 6kB. Der atmega328 hat aber nur 32 kB, dann kannst du dir ausrechnen wie viele Animationen in den Speicher passen. Ein solches Lauflicht kann man alternativ auch mit 3 Zeilen C-Code zu je maximal 20 byte Maschinencode umsetzen. Ein Bytecode wäre da also viel effizienter.