Private-Member angeben



  • Blöder Titel.

    Wenn ich eine Klasse in eine DLL exportiere und diese dann in meinem Programm nutzen will, muss ich in der Header-Datei, die die Klassendefinition hat, auch die private Member mit angeben?


  • Mod

    Prinzipiell musst du alles angeben. Falls es unbedingt sein muss, gibt es auch Techniken, um solche Details zu verstecken. Das Stichwort lautet "Pimpl". Die Technik kommt jedoch in der Regel mit (geringen) Laufzeitkosten!

    Ein guter Grund, dies trotzdem zu wollen, ist, Abhängigkeiten aufzubrechen. Wenn die Implementierungsdetails für die Außenwelt unsichtbar sind, dann kann man die Implementierungsdetails auch ändern, ohne dass die Außenwelt davon erfahren muss. Sprich: Wenn du an den privaten Dingen etwas änderst, muss Code, der die Klasse benutzt, nicht neu übersetzt werden.

    Kein guter Grund, dies zu wollen, ist, wenn du deine Implementierung aus Paranoia geheim halten willst. Wer wirklich etwas über deine Implementierungsdetails wissen möchte, kann diese auch aus dem übersetzten Code extrahieren.



  • Ich dachte, es ist eine Ja-/Nein-Frage. Habe aber interssante Artikel zu dieserr Methode gefunden, danke für den Hinweis.

    Und doch, es geht um Paranoia. Aber vor allem darum, dass sich die private/protected-Member ab und zu mal ändern/erweitern und ich mir dachte, dass ich nicht immer jedes Projekt neu kompilieren müsste, was die DLL mit der Klasse nutzt, nur weil sich ein für das Projekt an sich unwichtiges Detail geändert hat (schließlich greife ich ja in den Projekten eh nie direkt auf die private-Member zu, sonst könnte ich die ja gleich public machen). Dann brauch ich nicht immer und überall die Header tauschen.


  • Mod

    Private schrieb:

    Und doch, es geht um Paranoia. Aber vor allem darum, dass sich die private/protected-Member ab und zu mal ändern/erweitern und ich mir dachte, dass ich nicht immer jedes Projekt neu kompilieren müsste, was die DLL mit der Klasse nutzt, nur weil sich ein für das Projekt an sich unwichtiges Detail geändert hat (schließlich greife ich ja in den Projekten eh nie direkt auf die private-Member zu, sonst könnte ich die ja gleich public machen). Dann brauch ich nicht immer und überall die Header tauschen.

    Was denn nun? Du nennst den schlechten Grund und beschreibst den guten Grund.



  • Okay. Der erste Grund ist für mich kein Grund, was außer mir aber natürlich keiner wissen kann. Da ich nicht der Meinung bin, weltverändernden, finanziell höchst profitablen oder enorm sicherheitskritischen Code zu schreiben, hält sich auch meine Paranoia in Grenzen. In den zwei Fällen, in denen ich bisher mal nach einer Implementierung gefragt wurde, habe ich meinen eigentlich Nicht-OpenSource-Code auch so rausgerückt.



  • Private schrieb:

    Aber vor allem darum, dass sich die private/protected-Member ab und zu mal ändern/erweitern und ich mir dachte, dass ich nicht immer jedes Projekt neu kompilieren müsste, was die DLL mit der Klasse nutzt, nur weil sich ein für das Projekt an sich unwichtiges Detail geändert hat (schließlich greife ich ja in den Projekten eh nie direkt auf die private-Member zu, sonst könnte ich die ja gleich public machen). Dann brauch ich nicht immer und überall die Header tauschen.

    Dann ist PIMPL genau das richtige für dich.
    Nicht umsonst trägt das Idiom auch den Namen "compilation firewall".
    Siehe https://en.wikipedia.org/wiki/Opaque_pointer

    Eine andere Möglichkeit ist rein abstrakte Klassen zu verwenden - im Prinzip das selbe wie Interfaces in anderen Sprachen.
    Dabei gibst du dem User keine Möglichkeit ein Objekt einer Klasse direkt zu erzeugen, sondern stellst ihm nur Factories (Factory-Funktionen, Factory-Objekte) zur Verfügung, mittels derer Objekte erzeugt werden können.

    Die grössten Unterschiede zwischen PIMPL und "Interfaces" sind:
    PIMPL ist etwas schneller, die Kosten eines Funktionsaufrufs sind geringer. Wenn keine DLL-Grenzen dazwischen sind verhindert PIMPL nichtmal Inlining.
    "Interfaces" sind dafür flexibler, denn die Factory ist nicht auf eine konkrete Klasse eingeschränkt, sondern sie kann eine beliebige Klasse zurückliefern die nur von der Interface Klasse abgeleitet sein muss.
    D.h. man kann parallel verschiedene Implementierungen haben, und sich zur Laufzeit dann aussuchen welche man verwenden möchte.

    Wobei man diese Flexibilität nicht überbewerten sollte wenn man sie nicht gleich braucht. Im Falle des Falles kann man Code der PIMPL verwendet nämlich relativ einfach umschreiben. Wenn man möchte muss man dazu nichtmal die Schnittstelle der gePIMPLten Klasse ändern - der "pointer to implementation" wird dann einfach nur zu einem Zeiger auf eine abstrakte Basisklasse - das "Interface" eben.



  • Ich fände es manchmal toll, wenn man aus Gründen des "Compiler-Firewallings" eine public-Header-Datei haben könnte und eine separate für den ganzen private-Krams (ggf. entsprechend für protected), also ohne den Umweg über die pimpl-Klasse und die damit verbundenen falschen const-Bezüge.

    Somit wäre nur die Public-Headerdatei für die Aufrufer sichtbar, der Rest bliebe verborgen.

    (Klar, geht nicht, weil von außen nicht sichtbar wäre, wie groß ein Objekt denn nun wird. Da müsste der Compiler den privaten Kram dann wieder transparent in einer Art pimpl/allocator-Logik auslagern...)

    Und es würde nicht wirklich die Übersichtlichkeit und Handhabbarkeit erhöhen...


  • Mod

    minastaros schrieb:

    (Klar, geht nicht, weil von außen nicht sichtbar wäre, wie groß ein Objekt denn nun wird. Da müsste der Compiler den privaten Kram dann wieder transparent in einer Art pimpl/allocator-Logik auslagern...)

    So schwierig umsetzbar klingt das nicht, man könnte ja einfach die Größe angeben, ohne weitere Details*. Aber das wäre nur zum Wohle der Paranoiden, denn wenn die verborgenen privaten Details ihre Größe oder andere technisch relevanten Eigenschaften ändern, müsste man dann trotzdem neu übersetzen. Aber Paranoia ist, wie gesagt, ein schlechter Grund.

    *: Eine mögliche Pimpl-Umsetzung ist auch schon recht ähnlich, indem man nämlich das Datenobjekt nicht etwa langsam auf dem Heap allokiert, sondern mit std::aligned_storage direkt im Objekt.



  • SeppJ schrieb:

    *: Eine mögliche Pimpl-Umsetzung ist auch schon recht ähnlich, indem man nämlich das Datenobjekt nicht etwa langsam auf dem Heap allokiert, sondern mit std::aligned_storage direkt im Objekt.

    Genau das mache ich, um die <windows.h> aus meinen Dateien rauszuhalten.



  • SeppJ schrieb:

    ... Aber das wäre nur zum Wohle der Paranoiden, denn wenn die verborgenen privaten Details ihre Größe oder andere technisch relevanten Eigenschaften ändern, müsste man dann trotzdem neu übersetzen. ...

    Ich meinte auch nicht wegen der Paranoia, sondern um ein erneutes Compilen zu vermeiden.

    Nur eben, dass man die "versteckten" Member tatsächlich wie echte private verwenden kann, ohne Umweg über das Pimpl-Objekt, den damit verbundenen höheren Schreibaufwand, den Zugriff aus den privaten Funktionen zurück auf die public-Member der Mutter-Klasse, und die richtige const-Deklaration (eine Methode der Mutter-Klasse kann const sein, aber dennoch die Member der pimpl-Klasse ändern, was ja eigentlich ideell die privaten Member der Mutter-Klasse sind).

    Wie gesagt, nur so eine Idee, was wäre wenn...



  • Danke für die ganzen Hinweise. Ich wollte das jetzt gerade auf meine Klassen anwenden, aber das ist wesentlich mehr Arbeit, als ich dachte. Besonders public-Member (also Variablen) zu verarzten, grenzt ja beinahe an Körperverletzung. Entweder überlade ich den = Operator für die entsprechenden Datentypen oder (und das ist mir lieber, aber eben auch aufwendig) ich prüfe bei jedem Methodenaufruf, ob die benötigten public-Member in der pImpl-Klasse mit den Membern aus der eigentliche Klasse übereinstimmen und dann muss ich sie dementsprechend anpassen.

    Also besser gleich rumpImpeln, wenn nötig.


  • Mod

    private schrieb:

    Danke für die ganzen Hinweise. Ich wollte das jetzt gerade auf meine Klassen anwenden, aber das ist wesentlich mehr Arbeit, als ich dachte. Besonders public-Member (also Variablen) zu verarzten, grenzt ja beinahe an Körperverletzung. Entweder überlade ich den = Operator für die entsprechenden Datentypen oder (und das ist mir lieber, aber eben auch aufwendig) ich prüfe bei jedem Methodenaufruf, ob die benötigten public-Member in der pImpl-Klasse mit den Membern aus der eigentliche Klasse übereinstimmen und dann muss ich sie dementsprechend anpassen.

    😕
    Bist du sicher, dass du das Konzept richtig verstanden hast?



  • Nein bin ich nicht. Meine Annahme: Ich erstelle eine Klasse, die die eigentliche Klasse über einen Pointer kapselt. Um jetzt aber die Methoden aufrufen zu können, muss ich entweder das Objekt der eigentlichen Klasse in Verbindung mit der pImpl-Klasse verwenden, oder ich lege Methoden an, die die Methoden der eigentlichen Klasse aufrufen.


  • Mod

    Deine Beschreibung ist richtig, aber deine oben von mir zitierten Probleme bei der Umsetzung klingen geheimnisvoll.



  • Wenn du die ganze Klasse in der Hilfsklasse kapselst ist das natürlich viel Arbeit. Ich habe das Pimpl idiom zwar selbst noch nicht benutzt, würde aber nur private Membervariablen und die eine oder andere Funktion, die der Benutzer sowieso nicht aufrufen soll, in die Hilfsklasse packen. Vor allem aber keine public Variablen, denn die sind ja Teil des Interface. Wenn eine neue publice Variable hinzugefügt wird muss man eh alles neu compilieren weil das Objekt seine Größe verändert hat.



  • Da war der Denkfehler. Ich muss ja nich alles kapseln, sondern nur das, was intern innerhalb der Klasse genutzt wird (also die private-Member).

    Da stellt sich mir noch eine stilistische Frage:
    Sollte ich eher ein struct oder eine class nehmen, um die private-Member auszulagern? Theoretisch kennt ja C keine Funktionen als struct-Member (höchstens Funktionszeiger). Wenn ich mich nicht irre, interpretiert der Compiler ein struct eh als Klasse, wenn sobald ich eine Methode definiere (vorausgesetzt, ich compiliere alles als C++-Projekt und nicht als pure-C). Vom "Geschmack" her, wäre mir ein struct lieber, da es direkt anzeigt, dass keine hohe Abstraktion in den Gekapselten vorherrscht. Zumindest empfinde ich persönlich ein struct als weniger abstrakt als eine class. Wie seht ihr das?



  • Und wenn wir schon dabei sind: wie benennt Ihr denn eure opaquen Pointer? Ich würde den Namen in Bezug zur Zugehörigkeit setzen, wenn ich bspw. sowas machen würde:

    class NICEGUY_INTERN;    // opaque
    class NICEGUY {
    private:
        NICEGUY_INTERN *ngi;
    };
    

    Allerdings wäre die Definition von NICEGUY_INTERN dann überall dort gültig, wo ich die Datei einbinde (unschön, wie ich finde). Lieber ist mir da eher:

    class NICEGUY {
    private:
        class NICEGUY_INTERN;    // opaque
        NICEGUY_INTERN *ngi;
    };
    

    Hier wäre NICEGUY_INTERN zwar auch überall verfügbar, aber nur, wenn ich zuvor NICE_GUY als Raum angebe (also NICEGUY::NICEGUY_INTERN). Daher wäre mir hier ein etwas allgemeinerer Name schon lieber. Und da nochmal die Frage: Wie würdet ihr das nennen? OPAQUE ist ja teilweise schon belegt, auch OPAQUE_PTR wäre mir persönlich nicht all zu sicher (wer weiß, was irgendwann mal kommt). Allerdings finde ich hier eine verwendungs- bzw. absichtsbezogene Bezeichnung besser, als eine zugehörigkeitsbezogene.


  • Mod

    Wenn du C++ programmierst, wieso lässt du dich dann von einer komplett anderen Sprache beeinflussen, bloß weil diese auch ein "C" im Namen hat? struct hat eine ganz bestimmte Bedeutung in C++; die Bedeutung in C ist vollkommen irrelevant bei der Entscheidung ob class oder struct.

    Und bevor du fragst: Der einzige Unterschied zwischen struct und class ist, dass bei struct die Sichtbarkeit standardmäßig public ist, bei class hingegen private.



  • Private schrieb:

    Hier wäre NICEGUY_INTERN zwar auch überall verfügbar, aber nur, wenn ich zuvor NICE_GUY als Raum angebe (also NICEGUY::NICEGUY_INTERN).

    Nicht wirklich. Die Forward Declaration von NICEGUY_INTERN ist auch private, kann man von außen also nicht viel mit anstellen. Ich würde es daher auf jeden Fall in die Klasse packen. Wie du es nennst musst du selbst wissen, da Geschmackssache. Mir gefallen deine Klassennamen aus nur Großbuchstaben z.B. nicht. Großbuchstaben sind bei mir für böse Makros reserviert.



  • SeppJ schrieb:

    Und bevor du fragst: Der einzige Unterschied zwischen struct und class ist, dass bei struct die Sichtbarkeit standardmäßig public ist, bei class hingegen private.

    Ach da war sie wieder, die Unwissenheit. Danke für die Erhellung. Dann war ja meine Beschreibung, das ein struct in C++ ebenfalls als Klasse interpretiert wird insoweit richtig. Nur, dass ich es nicht wusste (und auch die Zusammenhänge nicht hätte erklären können), sondern es bisher immer nur beobachtet habe.

    SeppJ schrieb:

    Wenn du C++ programmierst, wieso lässt du dich dann von einer komplett anderen Sprache beeinflussen, bloß weil diese auch ein "C" im Namen hat?

    Gutes Argument. Ich vermute, weil Compiler heutzutage einfach alles fressen, egal ob es C oder C++ heißt. Solange die entsprechenden Includes da sind, lässt sich alles leicht vermischen (wobei ich dir zustimme: man sollte es nicht vermischen). Ich werde zukünftig drauf achten.

    sebi707 schrieb:

    Wie du es nennst musst du selbst wissen, da Geschmackssache.

    Na tolle Wurst, das war mir auch klar. Danke für deine weiteren Anmerkungen (besonders für den Hinweis, dass die forward declaration in der Klass auch private ist), die Großschreibung habe ich mir glaube ich irgendwann mal von der WinAPI abgeguckt. Wobei ich dir Recht gebe: so wirklich gefallen hat mir die komplette Großschreibung noch nie (besonders bei langen Bezeichnern hilft nur ein Unterstrich oder drei mal lesen, wie z.B. INITCOMMONCONTROLSEX)


Log in to reply