Forum: PC-Programmierung C++ Spezialisierung zur Laufzeit


von Patrick B. (p51d)


Lesenswert?

Hallo zusammen

Ich habe ein kleines Design Problem, und weiss nicht wie ich es am 
besten lösen soll:
Sagen wir ich habe 3 Geräte, die über eine RS485 Verbindung an den PC 
angeschlossen werden können. Das Kommunikationsprotokol und die 
Basis-Funktionen (z.B. FW-Update) sind bei allen gleich.

Desswegen bin ich bis jetzt so vorgegangen:
1
// Initialize basic slave stack
2
ISerialDriver& roSerialDriver = *new CSerialDriver();
3
CModbusRtuMaster& roModbusMaster = *new CModbusRtuMaster(roSerialDriver);
4
CDevice* poDevice = new CDevice(roModbsMaster);

Die Klasse CDevie stellt die Basis-Funktionen bereit. Man könnte es als 
"allgemeines" Gerät bezeichnen.

Ob an der Schnittstelle dann auch wirklich ein solches Device 
angeschlossen ist, kann ich wie folgt ermitteln:
1
// Setup Modbusmaster, Serial-Driver and Device with 1MBaud
2
if(poDevice->bInitDevice(1000000) == true)
3
{
4
  // Initialize application stack
5
6
}
7
else
8
{
9
  // No Device detected...
10
  // Possible usage:
11
  // - as true RS485 device
12
  // - as Modbus Rtu device
13
}

Sobald ein Device identifiziert wurde, fängt mein Problem an:
Normalerweise würde hier über eine klassische Vererbung arbeiten, aber 
da ich von meinenm Programm mehrere unterschiedliche Devices unterstütze 
muss ich das reale Device erst nachträglich zur Verfügung stellen. Bei 
einer Vererbung entstehen aber unterschiedliche Instanzen von 
Basisklassen, was nicht sehr effizient ist.

Am Schluss könnte die Verwendung sein:
1
// Usage of Device
2
// Return values indicates the communication result and are ignored
3
(void) poDevice->oGetPhSensor(0).bEnable(true);
4
5
bool bValue = false;
6
(void) poDevice->oGetInput(3).bGetValue(bValue);
7
8
(void) poDevice->oGetOutput(0).bSetValue(false);
9
10
unsigned char u8Red = 0;
11
unsigned char u8Green = 0;
12
unsigned char u8Blue = 0;
13
(void) poDevice->oGetRGBSensor().bGetValues(u8Red, u8Green, u8Blue);

Mir fehlt der Zwischenschritt, der die effektive Applikation 
zusammenbaut anhand dessen was für ein Device angeschlossen ist. Die 
Hardware deviniert ob und wie viele Sensoren, Inputs un Outputs 
vorhanden.
Wie macht man so etwas normalerweise?
Oder gibt es eine bessere Lösung?

Gruss
Patrick

von Noch einer (Gast)


Lesenswert?

Wie effizient muss es sein? Erste Idee wäre, CDevice kümmert sich nur um 
das Kommunikationsprotokol und gibt die Pakete an einen DataHandler 
weiter.

Das if(poDevice->bInitDevice(1000000) == true) legt eine Instanz der 
passenden DataHandler Klasse an.

von Patrick B. (p51d)


Lesenswert?

Noch einer schrieb:
> Wie effizient muss es sein?

mhm, das ganze sollte schon möglichst schlank sein (weshalb ich mehrere 
Instanzen von der Basis vermeiden möchte, da pro Schnittstelle sowieso 
nur 1 Device möglich ist).
Die Grundidee ist, dass ich am Schluss Plattformabhängig bin. 
Theoretisch könnte ein kleiner Fitlet-PC oder etwas in der Art wie ein 
Raspberry genauso gut die Verwaltung übernehmen wie ein PC. Oder ein 
etwas stärkerer ARM-Controller.

von BobbyX (Gast)


Lesenswert?

Schwer zu verstehen, was du überhaupt machen willst. Aber anhand dessen, 
was du schreibst vermute ich, dass das Stichwort zu Lösung deines 
Problems heisst: virtuelle Methoden...

von Michael P. (mipo)


Lesenswert?

Virtuelle Methoden wurde schon genannt, abstrakte Basisklasse wäre 
vermutlich auch noch ein Stichwort

von mh (Gast)


Lesenswert?

Patrick B. schrieb:
> ISerialDriver& roSerialDriver = *new CSerialDriver();
> CModbusRtuMaster& roModbusMaster = *new
> CModbusRtuMaster(roSerialDriver);
> CDevice* poDevice = new CDevice(roModbsMaster);

Kannst du genauer erklären was du hier machen willst? Warum Referenzen? 
Warum Pointer? Warum new?

von Patrick B. (p51d)


Lesenswert?

mh schrieb:
> Kannst du genauer erklären was du hier machen willst? Warum Referenzen?
> Warum Pointer? Warum new?

Die Serielle Schnittstelle ISerialDriver ist als abstrakte Klasse 
umgesetzt welche von CSerialDriver implementiert wird. Somit hätte ich 
die Möglichkeit die Schnittstelle Systemabhängig zu erstellen (bei einem 
uC oder PC sieht diese ja anders aus).
Die Modbus-Klasse bildet die absolute Basis der Kommunikation. Der Grund 
für Referenzen ist, weil ich es so gelernt habe (Referenzen vor Pointer 
bevorzugen). Eine Referenz vereinfacht das hanling innerhalb der Klasse, 
da diese nie NULL sein kann.

Michael P. schrieb:
> Virtuelle Methoden wurde schon genannt, abstrakte Basisklasse wäre
> vermutlich auch noch ein Stichwort

Die Basisklasse sollte aber nicht rein "abstrakt" sein, da die 
Grundfunktionen (FW-Update...) trotzdem bereit gestellt werden sollen. 
Die effektiven Devices erweitern die Basisklasse mit ihren Funktionen 
und Registeradressen.

von tictactoe (Gast)


Lesenswert?

Patrick B. schrieb:
> Die
> Hardware definiert ob und wie viele Sensoren, Inputs und Outputs
> vorhanden.

Ich denke, dass genau deshalb es nicht notwendig ist, vollkommen 
dynamisch zu sein. Es entscheidet sich nicht erst zum Zeitpunkt der 
Initialisierung, welches Device dran hängt, sondern das ist doch vorher 
schon klar. Wenn du trotzdem nur ein Stück Software schreiben willst, 
das alle Hardwaremöglichkeiten umfasst, dann sieh vor, eine 
Konfigurationsvariable aus dem EEPROM auszulesen, in der steht, welche 
Devices instanziiert werden müssen.

Oder ist es so, dass der User die Hardware beliebig umstöpseln kann? 
Dann wird dir wohl nichts anderes übrig bleiben, als eine 
Software-Komponente zu schreiben, die feststellt, was dran hängt, und 
entsprechende Device-Driver instantiiert.

Übrigens, nur weil du verschiedene Klassen von der gleichen Basis 
ableitest, hast du noch lange keine Instanzen. Von ineffizient kann man 
zu diesem Zeitpunkt noch nicht sprechen. Ineffizient ist es erst, wenn 
du Variablen anlegst, die du gar nicht brauchst, weil die Hardware nicht 
dran hängt.

von Patrick B. (p51d)


Lesenswert?

tictactoe schrieb:
> Oder ist es so, dass der User die Hardware beliebig umstöpseln kann?
> Dann wird dir wohl nichts anderes übrig bleiben, als eine
> Software-Komponente zu schreiben, die feststellt, was dran hängt, und
> entsprechende Device-Driver instantiiert.

Der User könnte vor dem Programmstart eine beliebige Hardware 
anstöpseln. Während dem Betrieb bleibt diese aber fix.

Wie könnte so eine Komponente aussehen?

tictactoe schrieb:
> Ineffizient ist es erst, wenn
> du Variablen anlegst, die du gar nicht brauchst, weil die Hardware nicht
> dran hängt.

Genau das ist doch aber das Problem, oder nicht?
Wenn ich etwas in der Art mache, habe ich doch 2 Instanzen der 
Basisklasse, wovon eine nicht verwendet wird. Zusätzlich würde die reale 
Device Instanz von einem ebenfalls nicht verwendet:
1
class CIoKarte : public CDevice
2
{
3
}
4
5
class CAnalogInput : public CDevice
6
{
7
}
8
9
10
CIoKarte1 oIoKarte();
11
CAnalogInput oAnalogInput();
12
if(oIoKarte.bConnected())
13
{
14
  // IO device card detected
15
  // oAnalogInput not used
16
}
17
else if(oAnalogInput.bConnected()
18
{
19
  // Analog device card detected
20
  // oIoKarte not used
21
}
22
else
23
{
24
  // No valid devcie connected
25
}

: Bearbeitet durch User
von Micha (Gast)


Lesenswert?

Ich würde eher so in die Richtung gehen
1
CDevice MyDevice(...);
2
switch(MyDevice.Type()) 
3
{
4
 case(DigitalIO):
5
  {
6
    CDigitalIO & DigiDevice(MyDevice);
7
    DigiDevice...
8
  }
9
 case(AnalogIO):
10
  {
11
    CAnalogIO & AnalogDevice(MyDevice);
12
    AnalogIO...
13
  }
14
 default:
15
   Error(..)
16
}

Kommt drauf an, wie viel Funktionalität in den Devices gemeinsam ist 
oder ob man tatsächlich komplett verschiedene Klassen bauen sollte

von Oliver S. (oliverso)


Lesenswert?

Patrick B. schrieb:
> Wenn ich etwas in der Art mache, habe ich doch 2 Instanzen der
> Basisklasse, wovon eine nicht verwendet wird. Zusätzlich würde die reale
> Device Instanz von einem ebenfalls nicht verwendet:

Das wäre natürlich fürchterlich. Nicht benutzte Daten im Speicher 
verfaulen nach kürzester Zeit, und dann laufen die Elektronen aus ...

Was genau sind denn deine Effizienzbedenken?

Oliver

von Vincent H. (vinci)


Lesenswert?

Als erstes sollten wir mal alle unser Vokabular einigen, eine 
"Spezialisierung" ist in C++ meiner Meinung nach nämlich keine 
abgeleitete Klasse, sondern eine Template Spezialisierung, die perse mit 
Vererbung einmal nichts zu tun hat.

Und langsam wird mir damit glaub ich auch klar was du eigentlich willst. 
Du suchst eine simple Möglichkeit verschiedene Instanzen von 
abgeleiteten Klassen zur Laufzeit anzulegen, aber eben wirklich nur die 
Instanzen die du brauchst. Das Problem hatten schon viele vor dir und 
deshalb gibts dafür ein zugeschnittenes Design-Pattern namens 
"Factory-Pattern" (sprich Fabrik).

In der einfachsten Form ist dieses Pattern eine freie oder statische 
Funktion, die abhängig von irgendwelchen Eingangsparametern dynamisch 
neue Instanzen erzeugt und diese zurückgibt.
1
struct CDevice {
2
  virtual void name() = 0;
3
};
4
5
struct Device1 : CDevice {
6
  virtual void name() override {
7
    std::cout << "Device1\n";
8
  }
9
};
10
11
struct Device2 : CDevice {
12
  virtual void name() override {
13
    std::cout << "Device2\n";
14
  }
15
};
16
17
std::unique_ptr<CDevice> factory(const int device_nr) {
18
  if (device_nr == 1)
19
    return std::make_unique<Device1>(Device1{});
20
  else if (device_nr == 2)
21
    return std::make_unique<Device2>(Device2{});
22
  // ...
23
}


Damit wird auch recht schnell ersichtlich, dass die Prüfung welches 
Device aktuell angeschlossen ist wohl nicht wirklich Teil der 
Device-Klasse selbst sein sollte.

Will man das dynamische Anlegen von Speicher verhindern, so liese sich 
theoretisch auch ein Summentyp (variant) an Stelle des Pointers als 
Rückgabewert nutzen. Der Summentyp würde nur so viel Speicher einnehmen, 
wie die größte abgeleitete Klasse benötigt und wäre somit Punkto 
Speichereffizienz irgendwo zwischen "dynamisch, so klein wie möglich" 
und "statisch, so groß wie möglich".

Ansonsten würde ich empfehlen ein wenig über C++11 und dynamische 
Speicherverwaltung zu lesen. Direktes new und delete sollten heutzutage 
nach Möglichkeit gemieden werden.


Referenzen auf dynamischen Speicher anzulegen ist in meinen Augen ein 
absolutes No-Go und Code Obfuscation in Reinstform!

: Bearbeitet durch User
von Johannes S. (Gast)


Lesenswert?

Patrick B. schrieb:
> Wenn ich etwas in der Art mache, habe ich doch 2 Instanzen der
> Basisklasse, wovon eine nicht verwendet wird.

Wenn sich aber erst zur Laufzeit entscheidet welche Devices tatsächlich 
vorhanden sind muss ja trotzdem der Code für alle möglichen Devices im 
Programm vorhanden sein, deshalb halte ich die Vererbung von einer 
Basisklasse für nicht verschwenderisch. Falls du das mit nicht optimal 
meintest.
Im ersten Post hast du das Interface im Konstruktor mitgegeben, dann 
hat das Device ein z.B. Modubus Interface. Bei der Vererbung sagst du 
das ist ein Modbus Device. Beides geht, hier würde ich das auch mit 
Vererbung machen. Je mehr in der abstrakten Basis drin ist (wie 
ID/Status liefern, read/write/reset) desto mehr kann man im Programm 
generisch mit den Devices umgehen und die switch/case Orgien vermeiden.
Wenn man die Basis komplett abstrakt macht (nur generisches read/write) 
könnte man auch verschiedene Klassen mit anderen Hardwareinterfaces 
erstellen.

von Daniel A. (daniel-a)


Lesenswert?

Also für mich hört sich diese Diskussion nach Vererbung vs Komposition 
an. Es gibt einen etwas dürftigen Wikipediaartikel, der das Pattern 
Komposition statt Vererbung anhand von Delegation zu erklären versucht:
https://en.wikipedia.org/wiki/Composition_over_inheritance
https://de.wikipedia.org/wiki/Komposition_an_Stelle_von_Vererbung

Meine meinung dazu ist, dass dies zwar ein sehr gutes Designprinzip ist, 
aber sich nicht sinvoll in Klassischen OOP Sprachen umsetzen lässt, 
wegen dem vielen Boilerplate code für das Delegation Pattern one welchen 
das ganze Projektdesign zusammenbrechen würde. Dafür zeigen Sprachen wie 
C oder Go hier ihre wahre Stärke: Durch die Trennung von Daten und 
Funktionen, bzw. das Fehlen von Methoden und Vererbung, entfällt das 
Delegation pattern, und man bekommt Komposition statt Vererbung gratis 
dazu. Beispiel:
1
struct cdevice {
2
  int fd;
3
};
4
5
struct device_a {
6
  struct cdevice* device;
7
};
8
9
struct device_b {
10
  struct cdevice* device;
11
};
12
13
bool cdevice_init(struct cdevice* dev){
14
  dev->fd = 0;
15
  return true;
16
}
17
18
bool cdevice_do_something(struct cdevice* dev);
19
20
void device_a_init(struct device_a* dev, struct cdevice* cdev){
21
  dev->device = cdev;
22
}
23
24
bool device_a_do_something_else(struct cdevice* dev);
Das Delegation pattern wird Hinfällig, weil man cdevice direkt verwenden 
kann: "cdevice_do_something(device_a->device)". In C ganz normal und 
kein Designproblem, in C++ eine Designkatastrophe. Das erst nachträglich 
bekannt wird, dass es ein device_a ist, spielt auch keine rolle, weil 
man ja Komposition verwendet und man durch die simple Verwenden eines 
Pointers die Lebenszeit der Objekte voneinander unabhängig machen kann.

von Patrick B. (p51d)


Lesenswert?

Vincent H. schrieb:
> Direktes new und delete sollten heutzutage
> nach Möglichkeit gemieden werden.

Wenn man die STL oder Boost verwenden kann, gibt es bessere Lösungen. 
Nur für den kleinen Embedded-Bereich sind STL und Boost sehr ungeeignet. 
Und wenn man den Code noch stark Platformunabhängig machen möchte (so 
dass auf Windows, Linux, SPS, uC...) das ganze kompilier bleiben soll, 
muss man leider auf solche Features verzichten.

Vincent H. schrieb:
> Und langsam wird mir damit glaub ich auch klar was du eigentlich willst.
> Du suchst eine simple Möglichkeit verschiedene Instanzen von
> abgeleiteten Klassen zur Laufzeit anzulegen, aber eben wirklich nur die
> Instanzen die du brauchst. Das Problem hatten schon viele vor dir und
> deshalb gibts dafür ein zugeschnittenes Design-Pattern namens
> "Factory-Pattern" (sprich Fabrik).

An so etwas hatte ich auch gedacht...

: Bearbeitet durch User
von Dr. Sommer (Gast)


Lesenswert?

Patrick B. schrieb:
> Nur für den kleinen Embedded-Bereich sind STL und Boost sehr ungeeignet.

Teile von Boost sowie der C++ Standard Library (so heißt das, was man 
meistens mit "STL" meint,  nämlich richtig) sind auch für Embedded kein 
Problem. Die Kunst besteht darin zu wissen, welche! Wenn "new" geht, 
sollten unique_ptr und vector z.B. auch gehen.

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.