Forum: PC-Programmierung Fragen zu C++-Design


von S. R. (svenska)


Lesenswert?

Hallo,

ich habe mal eine Frage zu C++. Oder eigentlich eine allgemeinere Frage, 
wie man ein Projekt halbwegs gebrauchbar objektorientiert strukturiert, 
ohne dass es auf Dauer eklig wird. Die Grundlagen von Objektorientierung 
kann ich, aber bei den Feinheiten wird's dünn. Daher frage ich hier.

Mir geht es nicht um Salamitaktik, der Hintergrund steht unten.

Gegeben ist eine zweistufige[1] C++-API, bestehend aus einem 
HidlProvider-Objekt, welches mehrere HidlDevice-Objekte bereitstellt. Da 
die HidlDevice-Objekte ziemlich eklige Datenstrukturen verwenden, hätte 
ich gerne eine Abstraktion dazwischen.

[1] Eigentlich dreistufig, aber die dritte Ebene ist erstmal 
uninteressant.

Erste Frage:
Mein Gedanke war, dass ich eine HidlDevice-Klasse implementiere und 
meine ganzen Devices davon erben. Die HidlDevice-Klasse implementiert 
dann die externe API (die Methoden müssen also public sein). Wie kann 
ich nun verhindern, dass meine Device-Klassen diese Methoden 
überschreiben oder aufrufen können? Geht das in C++ überhaupt?

Zweite Frage:
Wenn ich das so mache, dann muss ich im HidlProvider die jeweils 
konkreten Devices instantiieren. Je nach Anwendungsfall brauche ich aber 
unterschiedliche und unterschiedlich viele Devices, daher hätte ich 
gerne eine Tabelle, die jeweils die Namen und Typen(!) auflistet. Mit 
Makros ginge das, aber wie macht man das sinnvoll in C++?

Ich möchte das folgende vermeiden und hätte stattdessen gern eine 
Schleife:
1
  sp<HidlDevice> dev0 = TypeZeroDevice();
2
  if(dev0->isReady()) { mDevices.push_back(dev0); }
3
  sp<HidlDevice> dev1 = TypeOneDevice();
4
  if(dev0->isReady()) { mDevices.push_back(dev1); }
5
  // ...

Dritte Frage:
Kann ich das Wissen um die konkreten Typen vom HidlProvider trennen? 
Eigentlich muss der HidlProvider ja nicht wissen, von welchem Typ die 
einzelnen Devices sind, aber an irgendeiner Stelle muss ich ja die 
konkrete Klasse instantiieren...

Hintergrund: Es geht um eine HIDL-Schnittstelle in Android, konkret 
CameraProvider, CameraDevice und CameraSession (sowie die jeweiligen 
ICamera* und ICamera*Callback-Klassen). Die API ist komplex und 
verwendet unhandliche, serialisierte Datenstrukturen, daher hätte ich 
gerne eine eigene Abstraktion dazwischen. Hinten fällt ein einzelnes 
Binary raus, welches den Provider, alle Devices und

Wichtig ist mir, dass die einzelnen Devices möglichst wenig API-Code und 
Boilerplate beinhalten. Im Prinzip will ich den ganzen API-Layer mit dem 
nötigen Boilerplate einmal implementieren, aber dann getrennte 
Implementationen mit den konkreten Details separat haben. Am liebsten in 
einer Shared Library pro Device (wie das dann mit CameraSession läuft, 
weiß ich noch nicht; da passiert im Prinzip das gleiche umgekehrt - die 
getrennten Implementationen laufen wieder auf eine API-Implementation 
zusammen).

Aus meiner Sicht fällt das unter "advanced object orientation", und da 
fehlt mir Erfahrung. Daher frage ich hier.

Gruß,
Svenska

von MaWin (Gast)


Lesenswert?

Mehrfachvererbung, Polymorphismus

von Rolf M. (rmagnus)


Lesenswert?

S. R. schrieb:
> Hallo,
>
> ich habe mal eine Frage zu C++. Oder eigentlich eine allgemeinere Frage,
> wie man ein Projekt halbwegs gebrauchbar objektorientiert strukturiert,
> ohne dass es auf Dauer eklig wird.

Das fragen sich sehr viele. ;-)

> Wie kann ich nun verhindern, dass meine Device-Klassen diese Methoden
> überschreiben oder aufrufen können? Geht das in C++ überhaupt?

Überschreiben können sie nur, was in der Basisklasse virtual definiert 
wurde. Einen Aufruf kann man nicht verhindern. Aber wozu? Denkst du, 
dass so viele versuchen werden, deinen Code mit solchen Aufrufen zu 
sabotieren?

> Zweite Frage:
> Wenn ich das so mache, dann muss ich im HidlProvider die jeweils
> konkreten Devices instantiieren. Je nach Anwendungsfall brauche ich aber
> unterschiedliche und unterschiedlich viele Devices, daher hätte ich
> gerne eine Tabelle, die jeweils die Namen und Typen(!) auflistet.

Wenn du Polymorphie nutzt, ist der Typ automatisch dabei (RTTI).

> Dritte Frage:
> Kann ich das Wissen um die konkreten Typen vom HidlProvider trennen?
> Eigentlich muss der HidlProvider ja nicht wissen, von welchem Typ die
> einzelnen Devices sind, aber an irgendeiner Stelle muss ich ja die
> konkrete Klasse instantiieren...

Du speicherst in deiner Liste nur Zeiger auf die abstrakte Basisklasse. 
Die Instanziierung übernimmt eine Factory.

von Mikro 7. (mikro77)


Lesenswert?

S. R. schrieb:
> Erste Frage:
> Mein Gedanke war, dass ich eine HidlDevice-Klasse implementiere und
> meine ganzen Devices davon erben.

Vererbung sollte nur benutzt werden, wo wirklich nötig. In C++ wird sie 
häufig missbraucht, um nicht "nochmal" die ganzen Methoden deklarieren 
zu müssen. Siehe auch Composition (Has-A) vs. Inheritance (Is-A).

> Die HidlDevice-Klasse implementiert
> dann die externe API (die Methoden müssen also public sein). Wie kann
> ich nun verhindern, dass meine Device-Klassen diese Methoden
> überschreiben oder aufrufen können? Geht das in C++ überhaupt?

Also Composition nicht Inheritance. (Private Inheritance kann in sehr 
seltenen Fällen auch ein Lösung sein.)

> Zweite Frage:

Habe ich nicht verstanden. Wie sieht denn dein Makro-Ansatz aus? Häufig 
kommt man mit Templates weiter.

> Dritte Frage:
> Kann ich das Wissen um die konkreten Typen vom HidlProvider trennen?
> Eigentlich muss der HidlProvider ja nicht wissen, von welchem Typ die
> einzelnen Devices sind, aber an irgendeiner Stelle muss ich ja die
> konkrete Klasse instantiieren...

Hört sich aber nach einer Factory an, was du hier suchst. 
https://en.wikipedia.org/wiki/Factory_method_pattern

von Rolf M. (rmagnus)


Lesenswert?

MaWin schrieb:
> Polymorphismus

Es heißt Polymorphie. Polymorphismus ist ein Begriff aus der Gentechnik.

von Vincent H. (vinci)


Lesenswert?

S. R. schrieb:
> Erste Frage:
> Mein Gedanke war, dass ich eine HidlDevice-Klasse implementiere und
> meine ganzen Devices davon erben. Die HidlDevice-Klasse implementiert
> dann die externe API (die Methoden müssen also public sein). Wie kann
> ich nun verhindern, dass meine Device-Klassen diese Methoden
> überschreiben oder aufrufen können? Geht das in C++ überhaupt?

Das Interface sollte non-virtual sein (NVI Idiom) und ist damit nicht 
überschreibbar. Aufrufen lässt sich nicht vermeiden... aber wozu auch?

> Zweite Frage:
> Wenn ich das so mache, dann muss ich im HidlProvider die jeweils
> konkreten Devices instantiieren. Je nach Anwendungsfall brauche ich aber
> unterschiedliche und unterschiedlich viele Devices, daher hätte ich
> gerne eine Tabelle, die jeweils die Namen und Typen(!) auflistet. Mit
> Makros ginge das, aber wie macht man das sinnvoll in C++?

Du kannst die Devices innerhalb des HidlProviders ja als std::unique_ptr 
in irgendeinem Container speichern? Zum Beispiel in einer std::map. 
Damit wäre dann auch eine Namensgebung möglich.

> Dritte Frage:
> Kann ich das Wissen um die konkreten Typen vom HidlProvider trennen?
> Eigentlich muss der HidlProvider ja nicht wissen, von welchem Typ die
> einzelnen Devices sind, aber an irgendeiner Stelle muss ich ja die
> konkrete Klasse instantiieren...

In dem die Devices bereits außerhalb des HidlProviders erzeugt werden. 
Der HidlProvider bekommt nur noch fertige Devices, bzw. einen Container 
mit Devices. Je nach Komplexität kann man dafür schlichtweg Constructors 
verwenden oder eben die bereits vorgeschlagene Factory.

von S. R. (svenska)


Lesenswert?

Rolf M. schrieb:
>> Wie kann ich nun verhindern, dass meine Device-Klassen diese Methoden
>> überschreiben oder aufrufen können? Geht das in C++ überhaupt?
>
> Überschreiben können sie nur, was in der Basisklasse virtual definiert
> wurde. Einen Aufruf kann man nicht verhindern. Aber wozu? Denkst du,
> dass so viele versuchen werden, deinen Code mit solchen Aufrufen zu
> sabotieren?

Zumindest mein zukünftiges Ich würde dazugehören... :-)

Ein Design, was man prinzipiell nicht falsch verwenden kann ist mir 
trotzdem lieber.

Außerdem mag ich es nicht, wenn in einer Klasse irgendwelche Methoden 
rumliegen, die für die eigentliche Klasse nicht relevant sind. Das 
Device hat sich nicht dafür zu interessieren, welche Methoden das 
HidlDevice implementieren muss.

Rolf M. schrieb:
> Die Instanziierung übernimmt eine Factory.

Hmm. Klingt sinnvoll, muss ich mal drüber grübeln.

Mikro 7. schrieb:
> Also Composition nicht Inheritance.
> (Private Inheritance kann in sehr seltenen Fällen auch ein Lösung sein.)

Klingt auch sinnvoll. Muss ich mich mal einlesen, wie sowas 
funktioniert.

Mikro 7. schrieb:
>> Zweite Frage:
> Habe ich nicht verstanden. Wie sieht denn dein Makro-Ansatz aus?
> Häufig kommt man mit Templates weiter.

Aus der Hüfte, nicht implementiert:
1
#define MAKE_DEVICE(NAME, TYPE) do { \
2
    sp<HidlDevice> dev = TYPE(); \
3
    if(dev->isReady() { \
4
      mDeviceNames.push_back(NAME); \
5
      mDevices[NAME] = dev; \
6
    } \
7
  } while(0);
8
9
HidlProvider::HidlProvider() {
10
    MAKE_DEVICE("dev0", TypeZeroDevice);
11
}

Das Grundproblem ist: "Wie iteriere ich über eine Liste verschiedener 
Typen?"

Vincent H. schrieb:
> Das Interface sollte non-virtual sein (NVI Idiom) und ist damit nicht
> überschreibbar. Aufrufen lässt sich nicht vermeiden... aber wozu auch?

Idiotie. Ich kenne die vorhandene Codebasis und darin zu arbeiten ist... 
sehr wenig angenehm. Extrem viel Boilerplate, subtile 
Verhaltensabhängigkeiten zwischen den verschiedenen Teilen, etc.

Vincent H. schrieb:
> Du kannst die Devices innerhalb des HidlProviders ja als std::unique_ptr
> in irgendeinem Container speichern? Zum Beispiel in einer std::map.

Das mache ich auch, für die instantiierten Devices. Aber die Liste der 
zu instantiierenden Devices kann ich da nicht drin speichern, weil die 
existiert ja zu der Zeit nicht.

Vincent H. schrieb:
> In dem die Devices bereits außerhalb des HidlProviders erzeugt werden.

Hmm, das wird schwierig. Außen um den HidlProvider herum gibt es 
eigentlich keine Struktur mehr. Allerdings könnte er ja die Factory 
instantiieren und die dann laufen lassen... ich denke mal drüber nach.

Erstmal danke für die Anregungen.

von mh (Gast)


Lesenswert?

S. R. schrieb:
aus deinem
1
#define MAKE_DEVICE(NAME, TYPE) do { \
2
    sp<HidlDevice> dev = TYPE(); \
3
    if(dev->isReady() { \
4
      mDeviceNames.push_back(NAME); \
5
      mDevices[NAME] = dev; \
6
    } \
7
  } while(0);
8
9
HidlProvider::HidlProvider() {
10
    MAKE_DEVICE("dev0", TypeZeroDevice);
11
}

Solltest du ganz schnell so etwas wie
1
template<typeename T
2
void register_device(std::string const& name) {
3
    sp<HidlDevice> dev = T();
4
    if(dev->isReady() {
5
        mDeviceNames.push_back(name);
6
        mDevices[name] = dev;
7
    }
8
}
9
10
HidlProvider::HidlProvider() {
11
    register_device<TypeZeroDevice>("dev0");
12
}
machen. Über Typen iterieren macht man z.B. mit Typ-Listen. Da kannst 
entweder etwas fertiges nehmen oder etwas einfaches selbst erstellen.

von Mikro 7. (mikro77)


Lesenswert?

S. R. schrieb:
> Mikro 7. schrieb:
>> Also Composition nicht Inheritance.
>> (Private Inheritance kann in sehr seltenen Fällen auch ein Lösung sein.)
>
> Klingt auch sinnvoll. Muss ich mich mal einlesen, wie sowas
> funktioniert.

So wie ich es verstanden habe, hast du Devices, die alle recht ähnlich 
sind.

Die gemeinsame Funktion kommt dann in die "Core" class (quasi ein Lib). 
Die Schnittstelle, die die Devices zur Verfügung stellen ist das 
(abstrakte) "Interface". Die Implementierung eines "Zero" Devices wäre 
dann etwas wie
1
struct Zero : Interface
2
{
3
  // ... implement Interface methods ...
4
private:
5
  Core core ; // as member -- not inherited!
6
} ;

> Mikro 7. schrieb:
>>> Zweite Frage:
>> Habe ich nicht verstanden. Wie sieht denn dein Makro-Ansatz aus?
>> Häufig kommt man mit Templates weiter.
>
> Aus der Hüfte, nicht implementiert:

Wo siehst du das Problem, das "nur" durch ein Makro gelöst werden kann? 
In der Zeile: TYPE()? Das kann man doch direkt als Methode schreiben. 
Ggf. mit TYPE als Template argument (wobei ich das Gefühl habe, hier 
gehört iwo eine Factory rein).

> Das Grundproblem ist: "Wie iteriere ich über eine Liste verschiedener
> Typen?"

Da fehlt mir jetzt der Zusammenhang. Aber grundsätzlich: Entweder sie 
haben alle eine gemeinsame Basisklasse; oder du benutzt etwas wie 
boost::variant oder boost::any; oder...

von Vincent H. (vinci)


Lesenswert?

Ich hab das Problem noch nicht verstanden. Devices besitzen alle die 
selbe API, bzw. das selbe Interface, aber keine gemeinsame Basis? Dann 
kann dein Macro mit dem Array "mDevices" aber auch nicht funktionieren? 
(mit Standard C++ ohne jegliches Magic Unicorn TMP zumindest nicht)

von S. R. (svenska)


Lesenswert?

mh schrieb:
> S. R. schrieb:
> aus deinem
>> (Makrosalat)
> Solltest du ganz schnell so etwas wie
>> (Templatesalat)
> machen.

Da hast du natürlich recht, aber solche Makros kann ich aus der Hüfte 
schießen, Templates nicht. :-) Aber in beiden Fällen kann ich nicht über 
eine Liste iterieren, daher löst das nur ein Teil meines Problems.

> Über Typen iterieren macht man z.B. mit Typ-Listen.

Hmm. Das klingt alles ziemlich eklig.

Mikro 7. schrieb:
> So wie ich es verstanden habe, hast du Devices,
> die alle recht ähnlich sind.

Jaein. Wenn ich mehrere CameraDevices betrachte, dann unterscheiden die 
sich hauptsächlich in ihren Fähigkeiten, Charakteristika (Auflösungen, 
Frameraten, ...) und in den Registertabellen. Soweit nichts besonderes.

Aber dann gibt es Ausreißer: Sensoren, die andere Registerstandards 
benutzen. Extern konfigurierte Sensoren (d.h. die Auflösung wird vom 
Sensor vorgegeben, nicht vom System). Sensoren mit besonderen 
Fähigkeiten. Sensoren, die besondere ISP-Pipelines brauchen. Sensoren, 
die zwei Output-Ports haben. Und so weiter. Sensoren mit integriertem 
ISP (z.B. USB). Oder im Augenblick ein Dummy-Sensor komplett ohne ISP. 
:-)

Mit zunehmender Seltsamkeit der Hardware verschwinden die 
Gemeinsamkeiten. In jedem Fall bleibt aber "muss die 
CameraDevice/CameraSession-API implementieren" übrig, und die API stinkt 
ein wenig. :-)

Composition klingt spontan nach einer guten Lösung:
Ein HidlDevice enthält ein Device (mit der eigenen Implementation) und 
implementiert die API; der HidlProvider enthält nur Referenzen zu den 
HidlDevices, die er aus einer Fabrik holt.

Mikro 7. schrieb:
>> Das Grundproblem ist:
>> "Wie iteriere ich über eine Liste verschiedener Typen?"
> Da fehlt mir jetzt der Zusammenhang.

Das hat zwei Hintergründe:

Erstens: Wenn ich in meinem Gerät zwei TypeZero-Sensoren und einen 
TypeTwo-Sensor habe, dann muss ich die explizit hinschreiben. Ich hätte 
lieber eine externe Tabelle, wo ich für jedes Gerät angebe, welche 
Sensoren es gibt (bzw. welche Treiber geladen werden müssen).

Zweitens: Warum ich die HIDL-API nicht mag ist, dass ich keine 
Hashmap<Key,Value> für die Metadaten haben kann, weil jeder Key seinen 
eigenen Typen hat (oft ein Enum oder Array, manchmal was ganz anderes) 
und ich das in C++ nicht typsicher abbilden kann.

> Aber grundsätzlich: Entweder sie haben alle eine gemeinsame
> Basisklasse; oder du benutzt etwas wie boost::variant oder
> boost::any; oder...

Boost ist keine Option, und vom "sie haben alle eine gemeinsame 
Basisklasse" komme ich ja gerade her, weil das andere Probleme aufbaut. 
Eine andere Sprache ist übrigens auch keine Option: Eine C-API kann man 
von überall benutzen, eine C++-API nicht.

Vincent H. schrieb:
> Dann kann dein Macro mit dem Array "mDevices"
> aber auch nicht funktionieren?

Alle Devices implementieren die gleiche Schnittstelle, aber die 
Schnittstelle stinkt. Daher hätte ich gerne einen allgemeinen Wrapper 
drumherumgelegt, der die internen Strukturen passend konvertiert. 
Allerdings muss der Wrapper wissen, welche Devices es gibt und sie auch 
erzeugen - und das ist recht schwierig, wenn der Wrapper die 
Besonderheiten nicht kennen soll.

Das mDevices ist im Augenblick ein Pointer auf die Basisklasse, daher 
funktioniert das...

Wie gesagt, ich hab das in meinem Kopf noch nicht ganz aufgeräumt und 
nicht genug Wissen um OO-Design, um das aufzuräumen. Composition und 
Factory sind schonmal gute Stichwörter.

von mh (Gast)


Lesenswert?

S. R. schrieb:
>> Über Typen iterieren macht man z.B. mit Typ-Listen.
>
> Hmm. Das klingt alles ziemlich eklig.

Ist es nicht wirklich, vielleicht 10 Zeilen mit übersichtlichen 
Templates. Aber ich hab mir dein Problem nochmal angeguckt. Du willst ja 
nicht über Typen iterieren, sondern über Tuple(Name,Typ). Da man 
Stringliterale nicht zu "non type template parametern" machen kann, muss 
man mehr Aufwand treiben. Du musst die Liste mit Namen und Typen eh 
irgendwo im Quelltext haben, wo ist also das Problem mit einer "Liste" 
der Form:
1
register_device<Type1>("Name1");
2
register_device<Type2>("Name2");
3
register_device<Type3>("Name3");
4
register_device<Type4>("Name4");
5
...
Wenn du die Namen direkt in die Device-Klassen auslagern kannst, wird 
alles einfacher.

von Oliver S. (oliverso)


Lesenswert?

S. R. schrieb:
> Erstens: Wenn ich in meinem Gerät zwei TypeZero-Sensoren und einen
> TypeTwo-Sensor habe, dann muss ich die explizit hinschreiben. Ich hätte
> lieber eine externe Tabelle, wo ich für jedes Gerät angebe, welche
> Sensoren es gibt (bzw. welche Treiber geladen werden müssen).

Soll das dynamisch zur Laufzeit auf dem Gerät passieren, oder zur 
Compilezeit? Ganz oben schriebst du was von „hinten fällt ein Binary 
raus“.

Oliver

von S. R. (svenska)


Lesenswert?

Oliver S. schrieb:
> Soll das dynamisch zur Laufzeit auf dem Gerät passieren,
> oder zur Compilezeit? Ganz oben schriebst du was von
> „hinten fällt ein Binary raus“.

Eine HIDL-Implementation in Android besteht aus einem Binary, welches 
die ganzen Interfaces implementiert. Das wird zusammen mit dem Rest des 
Systems für ein bestimmtes Gerät gebaut, also zur Compilezeit sind alle 
möglichen Treiber und IDs bekannt.

Real gibt es dann noch Varianten, die man sinnvollerweise per #ifdef 
auswählt. Ich habe Code gesehen, der das mit einer Mischung aus 
Device-Trees, Makefiles, im Dateisystem verstreuten Dateien und #ifdef 
gemeinsam macht und würde das gerne vermeiden. (Unterschiedliche 
Hersteller.) :-)

mh schrieb:
> Wenn du die Namen direkt in die Device-Klassen auslagern
> kannst, wird alles einfacher.

Im Augenblick habe ich im HidlProvider eine Tabelle aus (int, 
string)-Paaren und iteriere über die. Der String wird dann an die 
HidlDeviceFactory übergeben, und da gibt es eine if/else-Kette, die die 
korrekte Klasse erzeugt.

Das reicht erstmal aus. Bisher habe ich genau null Implementationen, 
daher ist der Rest erstmal nebensächlich. :-)

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.