Grundlegende Fragen über das Design einer Shared Library in C++ mit Java im Hinterkopf
-
Nathan schrieb:
In C++ schreibt man zwei Klassen ObjectFoo und ObjectBar.
Leider bleiben damit folgende Fragen unbeantwortet:
- Wie schützte ich ObjectFoo und ObjectBar vor einer Instanzierung durch den Benutzer. Beide Klassen sollten nur durch eine Helperklasse (In diesem Fall die Factory) erstellbar sein, ansonsten können die Klassen missbraucht werden. ObjectFoo und ObjectBar sind wirklich reine Businessobjekte, sprich enthalten Daten und nicht wirklich Logik. Folglich möchte ich diese Objekte nur durch die Faktory verändern lassen (welche die Eingabe/Veränderung dann auch prüft)
- Was hide und was exportiere ich ? Ich möchte die Implementierung/Logik ja nicht exportieren, sondern nur die Schnittstelledot schrieb:
Auch wenn beide geschwungene Klammern verwenden: C++ ist nicht Java. Die beiden Sprachen sind extrem unterschiedlich. Ich würde empfehlen, dich erstmal von Grund auf tiefgehend mit C++ zu beschäftigen, ansonsten wirst du sehr wahrscheinlich einfach nur in C++ Java coden...
Den Gedanken habe ich auch oft. Mein Problem ist, dass ich eigentlich mit Qt zu C++ gekommen bin und nie wirklich mit C/C++, Hello World (Respektive Grundlegenden Sachen) als "Vanilla" angefangen habe. Es fällt mir jetzt recht schwer, wieder ohne Qt auszukommen, da mir dieses Framework einfach soviele praktische Features offeriert
dot schrieb:
pfui, Finger weg von Singleton. Auch wenn dies möglicherweise einer der extrem seltenen Fälle ist, wo man zumindest ein wenig argumentieren könnte, dass Singleton nicht zu 100% völlig daneben ist...
Geb ich dir vollkommen Recht. Ist jetzt rausgeworfen. Klasse wird einmal instanziert.
dot schrieb:
Ansonsten gibt es unter Windows z.B. auch COM, das wäre die ganz proper solution.
Kann ich leider nicht verwenden, ich möchte den Sourcecode plattformunabhängig halten. Darum verwende ich auch Frameworks (Unter anderem wie gesagt Qt). Defines für MSVC/gcc/clang exports wie cdecl oder atribute gehen aber vollkommen ok und gehören halt einfach dazu
Betreffend Factory/Interface/Export bin ich jetzt bei sowas gelandet (Was denke ich deiner Vorstellung entspricht ?). Jedenfalls funktioniert die LIBRARY_PRIVATE noch nicht - Defines für die Library wird gesetzt, bei der Applikation wird es weggelassen - soweit sollte es also hin kommen ?. Ich bin mir auch noch nicht ganz sicher ob das betreffend Vererbung überhaupt möglich ist:
main.cpp
#include <iostream> #include "library/ObjectExport.h" #include "library/ObjectFactory.h" #include "library/ObjectInterface.h" int main(void) { // Arbeitet wie gewünscht/gedacht ObjectFactory *factory = new ObjectFactory(); ObjectInterface *objectfoo = factory->createFoo(); ObjectInterface *objectbar = factory->createBar(); std::cout << "Factory:" << factory->getNumberAsCheck() << std::endl; std::cout << "ObjectFoo:" << objectfoo->getNumber() << std::endl; std::cout << "ObjectBar:" << objectbar->getNumber() << std::endl; delete objectfoo; delete objectbar; delete factory; // Dieses Szenarie gilt es zu verhindern (Funktioniert aber noch ...) ObjectFoo *abusedobject = new ObjectFoo(); std::cout << "AbusedObject:" << abusedobject->getNumber() << std::endl; delete abusedobject; return 0; };ObjectFactory.h
#ifndef OBJECTFACTORY_H #define OBJECTFACTORY_H #include "ObjectExport.h" #include "ObjectInterface.h" #include "ObjectFoo.h" #include "ObjectBar.h" class LIBRARY_EXPORT ObjectFactory { public: int getNumberAsCheck(); ObjectInterface *createFoo(); ObjectInterface *createBar(); }; #endif // OBJECTFACTORY_HObjectFactory.cpp
#include "ObjectFactory.h" int ObjectFactory::getNumberAsCheck() { return 9001; } ObjectInterface *ObjectFactory::createFoo() { return new ObjectFoo(); } ObjectInterface *ObjectFactory::createBar() { return new ObjectBar(); }ObjectInterface.h
#ifndef OBJECTINTERFACE_H #define OBJECTINTERFACE_H #include "ObjectExport.h" class LIBRARY_EXPORT ObjectInterface { public: virtual int getNumber() = 0; }; #endif // OBJECTINTERFACE_HObjectFoo.h (ObjectBar.h ist gleich)
#ifndef OBJECTFOO_H #define OBJECTFOO_H #include "ObjectExport.h" #include "ObjectInterface.h" class LIBRARY_PRIVATE ObjectFoo : public ObjectInterface { public: int getNumber() { return 41; } }; #endif // OBJECTFOO_HObjectExport.h (Momentan einfach mal für Linux GCC, Rest kommt wenn der Prototyp funktioniert)
#ifndef OBJECTEXPORT_H #define OBJECTEXPORT_H #if defined(COMPILER_GCC) #define LIBRARY_EXPORT __attribute__ ((visibility ("default"))) #define LIBRARY_PRIVATE __attribute__ ((visibility ("hidden"))) #else #define LIBRARY_EXPORT #define LIBRARY_PRIVATE #endif #endif // OBJECTEXPORT_HGruss
-
swaechter schrieb:
Nathan schrieb:
In C++ schreibt man zwei Klassen ObjectFoo und ObjectBar.
Leider bleiben damit folgende Fragen unbeantwortet:
- Wie schützte ich ObjectFoo und ObjectBar vor einer Instanzierung durch den Benutzer. Beide Klassen sollten nur durch eine Helperklasse (In diesem Fall die Factory) erstellbar sein, ansonsten können die Klassen missbraucht werden. ObjectFoo und ObjectBar sind wirklich reine Businessobjekte, sprich enthalten Daten und nicht wirklich Logik. Folglich möchte ich diese Objekte nur durch die Faktory verändern lassen (welche die Eingabe/Veränderung dann auch prüft)Konstruktoren. Memberfunktionen.
-
swaechter schrieb:
- Wie schützte ich ObjectFoo und ObjectBar vor einer Instanzierung durch den Benutzer. Beide Klassen sollten nur durch eine Helperklasse (In diesem Fall die Factory) erstellbar sein, ansonsten können die Klassen missbraucht werden.
Dann exportier sie einfach nicht. Wenn du sie nicht exportierst und dem User keine Deklaration der Klassen im Header deiner Library lieferst, bekommt der nichtmal mit, dass sie existieren. Überhaupt: Sowas wie Klassen gibt's sowieso nur im Source Code. Die fertig kompilierte Library enthält keinerlei Informationen über irgendwelche "Klassen" oder sonstwas, dort drin gibt's einfach nur einen Haufen Maschinencode.
swaechter schrieb:
dot schrieb:
Auch wenn beide geschwungene Klammern verwenden: C++ ist nicht Java. Die beiden Sprachen sind extrem unterschiedlich. Ich würde empfehlen, dich erstmal von Grund auf tiefgehend mit C++ zu beschäftigen, ansonsten wirst du sehr wahrscheinlich einfach nur in C++ Java coden...
Den Gedanken habe ich auch oft. Mein Problem ist, dass ich eigentlich mit Qt zu C++ gekommen bin und nie wirklich mit C/C++, Hello World (Respektive Grundlegenden Sachen) als "Vanilla" angefangen habe. Es fällt mir jetzt recht schwer, wieder ohne Qt auszukommen, da mir dieses Framework einfach soviele praktische Features offeriert
Nun, der von dir gepostete Code zeig die typischen Symtome: Überall wird new benutzt, sogar roh, alles wird per Referenz herumgereicht. Das ist halt genau, wie man es in Java macht, weil es dort nicht besser geht. Ein C++ Programmierer würde deine ObjectFactory z.B. nicht per new anlegen. Ein lokales Objekt tut's dort genauso. Ansonsten würde man Besitzverhältnisse über std::unique_ptr modellieren, anstatt mit rohen Zeigern und new. Dann spart man sich auch das ganze herumdeleten und alles ist automatisch exception-safe. Das Stichwort dabei lautet RAII und ist wohl eines der mächtigsten Feature von C++ überhaupt.
ACHTUNG: Vorsicht mit RAII! Wenn du RAII einmal verstanden hast, wirst du diese Welt niemals mehr verlassen wollen, jede andere Sprache wird dir mangels RAII früher oder später auf die Nerven gehen und jeder Kontakt mit Garbage Collection wird dich an den Punkt bringen, wo du dir am liebsten die Augen auskratzen würdest.

-
dot schrieb:
Dann exportier sie einfach nicht. Wenn du sie nicht exportierst und dem User keine Deklaration der Klassen im Header deiner Library lieferst, bekommt der nichtmal mit, dass sie existieren. Überhaupt: Sowas wie Klassen gibt's sowieso nur im Source Code. Die fertig kompilierte Library enthält keinerlei Informationen über irgendwelche "Klassen" oder sonstwas, dort drin gibt's einfach nur einen Haufen Maschinencode.
Ok, ich hatte das total falsch im Kopf. Der Datenheader wird dem Verwender ja nicht zur Verfügung gestellt. Soweit also ok
Betreffend RAII & (Smart) Pointern: Ist mir bewusst, aber es handelt sich hier nur um einen Prototypen. Ich sollte in Zukunft aber trotzdem mehr Wert auf Smart Pointers legen (RAII verwende ich wo es nur geht, ich habe einfach Angst, ich sprenge einmal den Stack)
Betreffend der ursprünglichen Frage: Die ist soweit gelöst, da ich auch auf das Thema PIMPL und/vs Abstract Interface gestossen bin. Ich werde die Factoryklasse wahrscheinlich mit dem PIMPL Gedanken realisieren und für die Businessobjekte Interfaces (also abstrakte Interfaces) zurückgeben.
-
Ist das zu viel verlangt, einfach die Konstruktoren zur korrekten Erzeugung zu nutzen und via Memberfunktionen die Daten wegzukapseln?
Ganz ohne Factory. Und ohne PIMPL, dessen Sinn und Zweck es eigentlich ist, nicht alle bneötigten Header zu inkludieren.
Und RAII hat keine Nachteile, die den "Stack sprengen könnten".
-
Nathan schrieb:
Ist das zu viel verlangt, einfach die Konstruktoren zur korrekten Erzeugung zu nutzen und via Memberfunktionen die Daten wegzukapseln?
Ganz ohne Factory. Und ohne PIMPL, dessen Sinn und Zweck es eigentlich ist, nicht alle bneötigten Header zu inkludieren.
Und RAII hat keine Nachteile, die den "Stack sprengen könnten".Nathan, mir bringt es leider nicht viel, wenn Du sagst: Mach es so, so und so, lass dies und jenes weg. Was mir wichtig ist:
- Warum so und nicht anderst ? Was ist sind deine Beweggründe, dies so zu realiseren ?
- Betreffend Header und exportieren einzelner Funktionen: Der Verwender hat ja dann doch den Header der Implementierung (Was ich nicht will). Wenn ich was falsch verstehe, mach doch bitte schnell ein Codebeispiel
- (Off Topic): Was passiert wenn ich mehrere grosse Listen auf den Stack werfe. Der Stack hat doch eine vom System vorgeschrieben [Vor dem Start änderbare] Grösse ?Gruss Simon
-
swaechter schrieb:
Nathan schrieb:
Ist das zu viel verlangt, einfach die Konstruktoren zur korrekten Erzeugung zu nutzen und via Memberfunktionen die Daten wegzukapseln?
Ganz ohne Factory. Und ohne PIMPL, dessen Sinn und Zweck es eigentlich ist, nicht alle bneötigten Header zu inkludieren.
Und RAII hat keine Nachteile, die den "Stack sprengen könnten".- Warum so und nicht anderst ? Was ist sind deine Beweggründe, dies so zu realiseren ?
Man spart sich Programmierarbeit, wenn man nur Klassen ohne Factory hat.
- Betreffend Header und exportieren einzelner Funktionen: Der Verwender hat ja dann doch den Header der Implementierung (Was ich nicht will). Wenn ich was falsch verstehe, mach doch bitte schnell ein Codebeispiel
Es wäre einfacher wenn du sagen würdest, was deine Klassen machen.
Nur mit dem jetzigen Beispiel lässt sich nur schwer Code zeigen.
Und ja, wenn du private Daten von einem Typ hast, die keine Pointer sind, muss der Header inkludiert werden. Aus rein programmiertechnischer Sicht ist das kein Problem, da der Client eh nicht (diverse Tricks jetzt mal außen vor gelassen) darauf zugreifen kann. Höchstens aus Wartungsgründen, da er dann recompilen muss, wenn sich die Implementierung ändert. Und deswegen gibt es PIMPL.- (Off Topic): Was passiert wenn ich mehrere grosse Listen auf den Stack werfe. Der Stack hat doch eine vom System vorgeschrieben [Vor dem Start änderbare] Grösse ?
RAII ist nicht "schmeiß alles auf dem Stack", RAII ist "lass Destruktoren aufräumen". Aber solange es keinen Grund gibt, etwas auf dem Heap zu werfen, lass es auf dem Stack. Der ist groß genug.
-
Nathan schrieb:
Es wäre einfacher wenn du sagen würdest, was deine Klassen machen.
Was ich im Kopf habe (Als Klassen):
- Platform: Kann Projekte erstellen/editieren/löschen und den Projekten Einträge hinzufügen/editieren/entfernen
- Project: Beinhaltet Name und Einträge als Liste
- Entry: Beinhaltet einen (Key)Namen und BeschreibungNun, Gedanken betreffend der Platform:
- Die Platform übernimmt das Inputchecking
- Der Verwender muss die Platform verwenden, um Projekte und Einträge ändern zu können
- Project und Entry sollen als reine Datenobjekte dienen, sprich keine Logik enthalten (Darum auch die Bearbeitung über die Platform)Wenn Du mir einen anderen Weg aufzeigen kannst, sowas zu realiseren, her damit

-
Also in etwa so?
struct project {...}; struct entry {...}; class platform { public: add_or_change_entry(size_t index, entry e); add_or_change_project(size_t index, project p); private: container projects_; container entrys_; };Was spricht dagegen, dass genau so zu machen?
Edit: Mit entry in project, hatte das überlesen.
-
Genau, etwas in der Art. Nun möchte ich aber die Implementierung von der Schnittstelle abtrennen, um zum Beispiel beim Hinzufügen/Änderung von privaten Attributen in der Implementierung nicht gleich das ganze Offset zu verschieben (Bin auf dem Gebiet neu & daran mich einzulesen).
Nun war meine Idee:
- Öffentliche Klasse besitzt einen Pointer zu der libraryinternen Implementierung
- Methodenzugriffe auf die öffentliche Klasse werden an die Implementierung weitergeleitet, diese arbeitet die Sache ab
- Änderungen in der Implementuerung machen eine Neukompilierung der Applikation mit der (neuen) Library überflüssig ?Oder habe ich da was falsch verstanden ? An dieser Stelle könnte ich doch das PIMPL Pinzip verwenden ?
Gruss Simon
-
swaechter schrieb:
Nun möchte ich aber die Implementierung von der Schnittstelle abtrennen, um zum Beispiel beim Hinzufügen/Änderung von privaten Attributen in der Implementierung nicht gleich das ganze Offset zu verschieben (Bin auf dem Gebiet neu & daran mich einzulesen).
Wozu? Wo liegt das Problem, wenn sich das Layout der Membervariablen ändert?
Was du beschreibst, ist gerade das PImpl-Idiom. Wie erwähnt wird es primär zur Verringerung von Compilezeit-Abhängigkeiten eingesetzt.
-
Nexus schrieb:
Wozu? Wo liegt das Problem, wenn sich das Layout der Membervariablen ändert?
Klar könnte ich darauf verzichten und immer davon ausgehen, dass Applikation und Library neu kompiliert & gelinkt werden, aber eventuell möchte ich das eines Tages ändern und dann alles umbauen, nur um das zu erreichen ? Lieber fange ich dann gleich richtig an und setze auf eine lose Kopplung zwischen Implementierung und Schnittstelle
Betreffend Businessobjekten: Gibt es da auch eine gute Möglichkeit, eine lose Kopplung zu erreichen oder schreibt man einfach eine Klasse, bestehend aus lauter virtuellen Funktionen und das eigentliche (interne) Businessobjekt erbt davon ? PIMPL würde in einem solchen Fall ja einen enormen Overhead generieren (Was mehr auffällt als bei einer Klasse, die aus lauter Logik besteht und Inpputchecking betreibt) ?
-
swaechter schrieb:
Klar könnte ich darauf verzichten und immer davon ausgehen, dass Applikation und Library neu kompiliert & gelinkt werden, aber eventuell möchte ich das eines Tages ändern und dann alles umbauen, nur um das zu erreichen ? Lieber fange ich dann gleich richtig an und setze auf eine lose Kopplung zwischen Implementierung und Schnittstelle
Nachträglich pimpl einfügen? Das ist quasi umsonst, da muss ja nicht einmal die Schnittstelle geändert werden.
PIMPL würde in einem solchen Fall ja einen enormen Overhead generieren
lol
-
justlol schrieb:
swaechter schrieb:
Klar könnte ich darauf verzichten und immer davon ausgehen, dass Applikation und Library neu kompiliert & gelinkt werden, aber eventuell möchte ich das eines Tages ändern und dann alles umbauen, nur um das zu erreichen ? Lieber fange ich dann gleich richtig an und setze auf eine lose Kopplung zwischen Implementierung und Schnittstelle
Nachträglich pimpl einfügen? Das ist quasi umsonst, da muss ja nicht einmal die Schnittstelle geändert werden.
PIMPL würde in einem solchen Fall ja einen enormen Overhead generieren
lol
Danke für deinen sehr konstruktiven Beitrag. Wenn Du schon solchen Content schreibst, registrier dich doch bitte, dass man dich erkennen kann (und ich dir konkret aus dem Weg gehen kann)
Betreffend PIMPL nachträglich einfügen:
1.) Was muss nicht mehr eingefügt werden ? Sollte es wie in Frage 2 sein hast Du definitiv kein PIMPL...
2.) Nein sicher nicht. Hast Du eine Klasse, in welcher gewisse Methoden exportiert und andere nicht exportiert werden, verschiebt sich dein Offset immer, wenn Du gewisse Bestandteile änderst (Methoden/Attribute etc). Das ist nicht gerade das, was ich anstrebe (Betreffend loser Kopplung).Vorherige Frage steht immer noch im Raum
-
Binärschnittstellen in C++ sind sowieso eine Angelegenheit für sich. Du bist wegen fehlendem standardisierten ABI relativ eingeschränkt und oft zum Neukompilieren gezwungen (spätestens wenn du den Compiler oder nur schon die Version oder bestimmte Flags wechselst).
Du solltest dir also überlegen, wie weit du deswegen dein Design verkomplizieren willst.
-
Hallo Nexus
Danke für deinen Antwort. Ich muss ein paar Sachen klarstellen und ergänzen, da ich erst seit gestern in das Thema reingekommen bin und sich der Thread doch recht entwickelt hat. Früher habe ich Libraries einfach exportiert und das wars - mehr nicht (Schnittstelle und Implementierung = Eine Klasse). Seit gestern habe ich mich vertieft damit auseinandergesetzt, um das ganze sauberer gestalten zu können. Zu ergänzen (meinerseits) ist:
Infrastruktur
- Ich verwende für die Applikation und die Library denselben Compiler und dasselbe CRT (Sprich MSVC und dessen CRT oder GCC/Clang und dessen GLIBC)
- Es kann vorkommen, dass die Applikation wegen Änderungen in der Library nicht neukompiliert wird (Wir reden über eine Patch Level Änderung in der Library, nicht Major & Minor, deshalb muss gewährleistet sein, dass sich das Offset nicht ändert (Z.B neue Variable wird eingeführt). ANMERKUNG: Ich habe mich noch nicht vertieft in Offset Changing eingelesen. Wird das ganze über eine lose Kopplung realisiert, funktioniert die Applikation auch mit der geupdateter Library. Verwende ich aber 1 Klasse wie bereits mehrmals in diesem Thread vorgeschlagen wurde, ist es (gut) möglich, dass sich das Offset der Library ändert [und das der Applikation nicht, da kein recompile].Das CRT zwischen Applikation und Library bleibt aber auch in diesem Fall gleich (Build Server)Betreffend deiner Aussage 1:
- Wird deine Aussage nicht eher unwichtig/rückt in den Hintergrund, wenn ich garantieren kann, dass ich denselben Compiler und dasselbe CRT verwenden kann. Es erfolgt definitv kein Compiler/CRT Mixing ! Entweder kompiliert das Buildsystem ALLES oder nur Änderungen, aber immer mit dem gleichen Compiler/CRT.Betreffend deiner Aussage 2:
Nexus schrieb:
Du solltest dir also überlegen, wie weit du deswegen dein Design verkomplizieren willst.
Wie darf man das verstehen. Könntest Du mir ein komplettes Codebeispiel geben, wie Du es machen würdest, wenn dir jemand den Auftrag gibt, eine Klasse zu schreiben, welche Projekte und Eintrage machen/editieren/löschen kann, WENN Du davon ausgehen kannst, dass der Compiler und das CRT IMMER dasselbe sind und eine lose Kopplung von Nöten ist ? Ich wär dir dafür sehr dankbar.
Schöne Weihnachten dir & Mitleser
Gruss Simon
-
Bezüglich ABI kompatiblen Änderungen in Klassen:
AFAIK ändert sich die ABI Schnittstelle einer Klasse nicht, wenn ein neuer Member(eine neue Memeber-Methode?) immer ans ende der Klassen definition gepackt wird.
Wobei ich nicht weis, ob sich die ABI eventuell ändert, wenn eine Änderung in einer Klasse gemacht wird, welche als Basis für andere Klassen Verwendung findet
-
firefly schrieb:
Bezüglich ABI kompatiblen Änderungen in Klassen:
AFAIK ändert sich die ABI Schnittstelle einer Klasse nicht, wenn ein neuer Member(eine neue Memeber-Methode?) immer ans ende der Klassen definition gepackt wird.
Bei nicht-statischen Datenmembern wird die Klasse dadurch bei allen Systemen die ich kenne inkompatibel, da sich
sizeof(T)ändert.Neue Methoden sind dagegen meist egal. So lange sie nicht virtuell sind, ist dabei auch egal wo man sie dazuschreibt. Und wenn sie virtuell sind, man sie aber brav hinter alle anderen virtuellen Funktionen schreibt, dann ist es auch egal. Zumindest eben auf den Systemen nicht ich kenne.
Hat aber mit Standard C++ nix zu tun, da der Standard diesbezüglich nix vorschreibt.
Wobei ich nicht weis, ob sich die ABI eventuell ändert, wenn eine Änderung in einer Klasse gemacht wird, welche als Basis für andere Klassen Verwendung findet
Wenn nicht-statische Datenmenber betroffen sind, ist die Chance gross, dass alle abgeleiteten Klassen neu übersetzt werden müssen. Wenn sich die Grösse oder das Layout der Klasse bzw. abgeleiteten Klassen dadurch ändert auch der gesamte Client Code.
Sowas sind aber Dinge über die ich mir üblicherweise nicht den Kopf zerbreche.
Für mich gibt es ein paar sinnvolle Varianten damit umzugehen:
* Reines C Interface
* COM
* PIMPL
* volles C++ Interface + immer alles neu ÜbersetzenZwischenlösungen mache ich da nur wenn ich wirklich muss. Was in den letzten Jahren nicht vorgekommen ist. Ich hab' aber auch nur mehr ein Projekt wo ich ein stabiles binary Inteface garantieren muss, und das verwendet COM.