Forum: Compiler & IDEs Spezialistenfrage: C++ Downcast


von Janos B. (janos)


Lesenswert?

Hallo zusammen,

ich bin nun schon einige Zeit dabei eine C++ Library zur umfassenden 
MIDI-Fernsteuerung eines Kemper Profiling Amps (ein digitaler E-Gitarren 
Verstärker mit massig Funktionen die man steuern kann) zu entwickeln. 
Ich hole ganz kurz zu den Hintergründen aus bevor ich zur eigentlichen 
Programmierungsfrage komme um meine Überlegung verständlicher zu machen. 
Wer mag darf den Abschnitt auch überspringen ;)


--------- Die Hintergründe -----------------------

Die Library, welche ich programmiere ist so ausgelegt, dass sie mit 
nahezu vollständig gleicher API und gleichen Funktionen sowohl auf 
PC-Architekturen als auch auf Mikrocontrollern (aktuell nur mit dem 
Arduino-Framework - prinzipiell aber alles an Hardware mit UART-Port 
wofür es einen C++ Compiler gibt) läuft. Das erreiche ich, indem ich 
eine Cross-Plattform-fähige MIDI-Library entwickelt habe, welche 
wiederum mit vollständig gleicher API die MIDI-Kommunikation über die 
jeweilige Hardwareschnittstelle der Wahl realisiert, das ist z.B. für 
Arduino ein HardareSerial oder SofwareSerial, für MacOS ein 
CoreMIDI-basiertes USB-MIDI-Inferface, diverse weitere Implementierungen 
sind in Planung.

Das hat den Vorteil, dass die Arbeit von mir und vllt auch. der 
Community für mehrere Projekte auf völlig verschiedenen Plattformen 
genutzt werden kann und dass ich entspannt die Ansteuerung einzelner 
Features auf dem PC entwickeln und debuggen kann und diese durch den 
hübschen Hardware Abstraction Layer nahezu ohne Probleme auf Anhieb auch 
auf dem Mikrocontroller laufen.

Nebenbei ist die ganze Geschichte Header-Only gehalten, so dass ein 
einziges #include-Statement reicht um alles zum laufen und kompilieren 
zu bekommen!

Dabei achte ich stets auf möglichst mikrocontroller-freundlich 
gestalteten Code und muss relativ wenig Performance-Einbußen in Kauf 
nehmen.

Nun hat der Amp einen Haufen ansprechbarer Parameter, welche Funktionen 
von Effekten Steuern, die abhängig vom aktuell geladenen Preset sich 
möglicherweise in den 8 virtuellen Effektslots befinden. Ist nun aber 
ein bestimmter Effekt nicht geladen, geht ein Steuerbefehl an diesen 
Effekt in diesem Slot ins leere.

Da dies so viele Parameter sind, würde ich ungern in der Hauptklasse 
dafür jeweils eine eigene setter- und getter-Methode implementieren. 
Stattdessen fände ich es sehr angenehm, mir eine weitere 
Effekt-Basisklasse zu erstellen und viele Klassen für die jeweiligen 
spezifischen Effekttypen, welche von der Basisklasse erben und die 
Ansprache der jeweils relevanten Parameter abstrahieren. Dazu würde ich 
dann gerne in der Hauptklasse jeweils Methoden implementieren nach der 
Art
1
StompTypeA *getStompTypeAInstance()
welche mir in dem Fall, dass ein solcher Effekt im aktuellen Setup 
geladen ist einen Pointer auf die passende Instanz geben um diesen 
Effekt zu kontrollieren oder falls ein solcher Effekt nicht geladen ist, 
einen Nullpointer zurück geben.

Nach jeden Preset-Wechsel scanne ich eh einmal die Effekt-Slots durch, 
weiß also welche Effekte jeweils vorhanden sind. Wäre ich nun 
ausschließlich auf dem PC unterwegs, würde ich mir dann jeweils auf dem 
Heap eine Instanz der passenden Effekt-Klasse anlegen und gut wäre es. 
Nun will ich aber Mikrocontoller-kompatibel völlig ohne Heap auskommen, 
also möchte ich die Infos über die geladenen Effekte gerne in einem 8 
Felder großem Stack-Array ablegen. Dies hat mich nach längerem hin- und 
her überlegen zu folgender Idee getrieben:


--------------- Die eigentliche Frage: ------------------------

Ich habe eine Basisklasse und eine gewisse Anzahl an Klassen die davon 
erben. Konkret in dieser Art:
1
class StompBase {
2
3
public:
4
5
    /**
6
     * Will be called once in the constructor of the Amp class
7
     */
8
    void setFixedPropertiesPtr (NRPNPage slotPage, KemperProfilingAmp *amp) {
9
        this->slotPage = slotPage;
10
        this->amp = amp;
11
    }
12
    
13
    /**
14
     * Will be called after each rig update to set the stomp type in that stomp slot
15
     */
16
    void setStompType (StompType stompType) {
17
        this->stompType = stompType;
18
    }
19
    
20
    StompType getStompType() {
21
        return stompType;
22
    }
23
24
    /**
25
     * Should be overriden by derived class to determine if the assumed stomp is still present in that slot
26
     * @return 
27
     */
28
    virtual bool isStillValid() {return false; };
29
30
protected:
31
    StompType stompType = Empty;
32
    NRPNPage slotPage = PageUninitialized;
33
    KemperProfilingAmp *amp;
34
};
35
36
37
class WahStomp : public StompBase {
38
39
public:
40
41
    /**
42
     * Checks if there still is a Wah stomp in this slot
43
     */
44
    bool isStillValid() override {
45
        if ((stompType == StompType::Wah) && (slotPage != PageUninitialized)) {
46
            return true;
47
        }
48
        return false;
49
    }
50
51
    /**
52
     * Sets the Wah range parameter
53
     */
54
    void setRange (uint8_t range) {
55
56
        if ((amp->lastNRPNPage != slotPage) || (amp->lastNRPNParameter != WahRange)) {
57
            amp->setNewNRPNParameter (slotPage, WahRange);
58
        }
59
60
        amp->sendControlChange (119, range);
61
    }
62
63
    // ... and a lot more...
64
};
65
66
class PhaserVibeStomp : public StompBase {
67
68
public:
69
70
    /**
71
     * Checks if there still is a PhaserVibe stomp in this slot
72
     */
73
    bool isStillValid() override {
74
        if ((stompType == VintageChorus) && (slotPage != PageUninitialized)) {
75
            return true;
76
        }
77
        return false;
78
    }
79
80
    /**
81
     * Sets the PhaserVibe modPahserStages parameter
82
     */
83
    void setModPhaserStages (uint8_t modPhaserStages) {
84
85
        if ((amp->lastNRPNPage != slotPage) || (amp->lastNRPNParameter != ModPhaserStages)) {
86
            amp->setNewNRPNParameter (slotPage, ModPhaserStages);
87
        }
88
89
        amp->sendControlChange (119, modPhaserStages);
90
    }
91
92
    // ... and a lot more...
93
};

Nun bestehen alle Klassen die davon erben immer ausschließlich aus 
Member Funktionen und fügen der Klasse keine einzige Member Variable 
hinzu. Wenn ich mir das vorstelle, müsste doch nun das Speicher-Layout 
von StompBase und beliebigen davon abgeleiteten Klassen nach der oben 
beschriebenen Machart exakt gleich aussehen. Demnach müsste ein Downcast 
von
1
StompBase*
 z.B. zu
1
WahStomp*
 doch problemlos funktionieren. ODER? Genau an diesem Punkt bin ich mir 
nicht ganz sicher und hätte gerne etwas einblick von jemanden der ganz 
tief im Compiler-Thema drin sitzt und mir sagen kann ob das immer 
funktionieren wird.

Falls das nämlich funktioniert, würde ich in der Hauptklasse so etwas 
implementieren:
1
/**
2
 * This array gets updated with the stomp types after each rig change
3
 */
4
 StompBase stompsInCurrentRig[8];
5
6
/**
7
 * Returns a pointer to a WahStomp instance if one exists, returns a nullptr otherwise
8
 */
9
 WahStomp *getWahStompInstance() {
10
    for (int i = 0; i < 8; i++) {
11
        if (stompsInCurrentRig[i].getStompType() == StompType::Wah) {
12
           return (WahStomp*) &stompsInCurrentRig[i];
13
        }
14
    }
15
16
    return nullptr;
17
 }

Damit könnte ich das im oberen abschnitt beschriebene Ziel hübsch 
(zumindest nach Außen hin :D) erreichen und dabei völlig 
Mikrocontroller-kompatibel ausschließlich auf dem Stack unterwegs 
sein...

Falls jemand einen Vorschlag hat nach außen hin das gleiche zu erreichen 
ohne solch einen fiesen Downcast-Hack freue ich mich auch!

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

Nein, das geht nicht, da die Klasse einen vtable-Pointer enthält, der 
nach deinem fiesen Cast aber nicht angepasst wurde. Es werden also immer 
nur die Basisklassen-Implementationen aller virtuellen Memberfunktionen 
aufgerufen.

Du könntest dir aber mal placement new ansehen. Damit kannst du aus 
einem bereits vorhandenen "rohen" Speicherblock (mit passendem 
Alignment) auf offiziellem Weg ein Objekt machen.

: Bearbeitet durch User
von Janos B. (janos)


Lesenswert?

Danke für die Antwort.

Rolf M. schrieb:
> Es werden also immer
> nur die Basisklassen-Implementationen aller virtuellen Memberfunktionen
> aufgerufen.

Bedeutet im Umkehrschluss, dass das gut ginge wenn ich mir in der 
Basisklasse die virtuelle Memberfunktion sparen würde und diese als 
gewöhnliche Member-Funktion in allen abgeleiteten Klassen nutzen würde?

Rolf M. schrieb:
> Du könntest dir aber mal placement new ansehen. Damit kannst du aus
> einem bereits vorhandenen "rohen" Speicherblock (mit passendem
> Alignment) auf offiziellem Weg ein Objekt machen.

Ah, das ist auch interessant, kannte ich noch nicht. Hab mir gerade mal 
oberflächlich ein paar Beispielcodeschnipsel angeschaut. Säh damit die 
Lösung vllt irgendwie so aus?
1
// Array to hold all stomps
2
StompBase stompMemory[8];
3
4
// I got the information that there is a Wah in stomp slot 5 so I add a Wah to stomp slot 5
5
WahStomp *newWahStomp = new ((void*) &stompMemory[4]) WahStomp;
6
7
/**
8
 * Returns a pointer to a WahStomp instance if one exists, returns a nullptr otherwise
9
 */
10
WahStomp *getWahStompInstance() {
11
    for (int i = 0; i < 8; i++) {
12
        if (stompsInCurrentRig[i].getStompType() == StompType::Wah) {
13
            return (WahStomp*) &stompsInCurrentRig[i];
14
        }
15
    }
16
17
    return nullptr;
18
}

Ich seh es richtig, dass es keinen Grund gibt delete aufzurufen bevor 
ich an den Platz ein neues Objekt anlege?

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Janos B. schrieb:

> Ich seh es richtig, dass es keinen Grund gibt delete aufzurufen bevor
> ich an den Platz ein neues Objekt anlege?

Der Dtor sollte immer aufgerufen werden, könnte ja sein, dass er was 
tut.

von Wilhelm M. (wimalopaan)


Lesenswert?

Du nutzt ja das dynamische Binden gar nicht aus und machst keinen 
dynamic_cast, sondern einen pre-check anhand eines Members und dann 
einen static_cast.

Dann kannst Du einfach ein Array der immergleichen PODs anlegen (mit dem 
tag-Member) und dann damit ein Menge von überladenen Funktionen 
aufrufen. Das wäre m.E. einfacher.

von BobbyX (Gast)


Lesenswert?

Anhand von dem was du schreibst und dem Code sehe ich, dass das was du 
machst ein sehr gutes Beispiel für Overengineering ist... ICh empfehle 
dir sich eine alte Version von SoundDiver von Emagic zu besorgen und zu 
sehen wie dort solche Dine gemacht wurden. In reinem C, aber 
mutliplatformfähig. Falls Du es nicht weisst: Emagic gehört heute zu 
Apple und die Leute von Emagic waren massgeblich an Entwicklung von 
CoreAudio beteiligt. Meiner Meinung nach die beste Audio/MIDI API die es 
gibt.

Mit deinem Ansatz kommen solche Monster wie der Editor vom Roland GR-55. 
Ich habe es mir mal angesehen. Ein Programm, dass ein bisschen MIDI hin 
ind her schiebt und im Idle-Zustand 10% Prozessorlast auf einem modernen 
PC eryeugt muss einfach schrott sein....

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.