Private-Member angeben



  • 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)



  • Private schrieb:

    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.

    Zumindest der C++ Compiler frisst das meiste, da C++ natürlich aus C entstanden ist und eine gewisse Rückwärtskompatibilität pflegt*. So wurde auch struct einfach aus C übernommen und um ein "Paar" Dinge wie Konstruktor/Destruktor, Memberfunktionen und Sichtbarkeit erweitert. Für die Rückwärtskompatibilität soll sich ein C++ struct ohne die neuen Features dann möglichst wie in C struct verhalten, weshalb auch die Sichtbarkeit standardmäßig public sein muss.

    *Ganz kompatibel ist C mit C++ nicht. So konnte man in C noch seine Variablen "new" und "class" nennen was in C++ natürlich nicht mehr geht. Außerdem gibts noch einige kleinere Unterschiede z.B. bei inline Funktionen.


  • Mod

    Private schrieb:

    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.

    Das ist absolut nicht der Fall. Viele Compiler können mehrere Sprachen in dem Sinne, als das mehrere verschiedene Compiler in der gleichen Executable untergebracht sind (oder genauer: mehrere Compiler stecken hinter einem einheitlichen Backend). Eventuell mit Heuristiken, die beispielsweise anhand von Dateiendungen erraten, was für eine Sprache benutzt werden soll. Aber ein C++-Compiler (oder ein Multisprachcompiler im C++-Modus) wird kein C-Programm verstehen und umgekehrt ebenfalls nicht*. Am ehesten geht noch ein C-Programm mit einem C++-Compiler zu übersetzen, so lange der C-Programmierer bewusst auf Dinge verzichtet hat, die in C++ nicht gehen.



  • Danke Euch beiden, wieder etwas schlauer (und nächstes mal hoffentlich genauer).


  • Mod

    sebi707 schrieb:

    *Ganz kompatibel ist C mit C++ nicht. So konnte man in C noch seine Variablen "new" und "class" nennen was in C++ natürlich nicht mehr geht. Außerdem gibts noch einige kleinere Unterschiede z.B. bei inline Funktionen.

    Du hast den wichtigsten Unterschied vergessen: In C sind implizite Pointerkonvertierungen von und nach void* gang und gäbe, in C++ sind sie hingegen nicht erlaubt. Daher wird die Übersetzung von C-Programmen mit einem C++-Compiler bereits da dran scheitern, wenn ein Programm solch "exotische" Funktionen wie malloc oder memcpy benutzt.

    Es gibt noch viel mehr Unterschiede (siehe C++-Standard, Anhang C*), aber obiger ist wohl der, an dem es meistens scheitern wird.

    *: Ich frage mich, ob da bewusste Kalkulation hinter steckt, das ausgerechnet im Anhang C zu beschreiben 🙂



  • Denk dran, dass das pimpl-Objekt an einem Pointer hängt. Das heißt, es muss mit erzeugt und am Ende mitsamt dem Mutter-Objekt auch wieder zerstört werden.

    Verwende also gleich einen smart pointer! ( http://stackoverflow.com/questions/5576922/pimpl-shared-ptr-or-unique-ptr )

    Logisch, dass Du dann auch an Dinge wie Deep-Copy denken solltest, je nachdem, was Deine Klasse so machen soll.

    Noch etwas weitere Literatur:
    http://herbsutter.com/gotw/_100/

    Persönliche Anmerkungen:

    * ich bevorzuge struct statt class . Die pimpl-Klasse ist ja eigentlich nur das Private der Mutterklasse, und die soll natürlich ohne weiteres drauf zugreifen können. Zudem geht es hier meist um Daten, weniger um selbständige Funktionalität. private macht da keinen Sinn, daher gleich das Format nehmen, bei dem per default alles public ist.

    * ich nenne das Pimpl-Objekt gerne pimpl ... Dann erkennt man es immer sofort.

    class Niceguy {
    private:
        struct Niceguy_intern;    
        std::unique_ptr<Niceguy_intern> pimpl;
    };
    

    Ich habe es übrigens schon erlebt, dass man den Destructor der Mutterklasse ausdrücklich deklarieren und definieren muss, sonst hatte der Compiler/Linker gemeckert (weiß nicht mehr genau, irgendwas mit missing reference). War ein älterer gcc... Wenn bei Dir alles geht, braucht Dich das nicht zu jucken.
    PS: es muss sogar sein! Herb Sutter erklärt in dem Artikel (link oben) auch, warum.



  • Danke für die weiterführenden Links, waren mitunter sehr interessant (wenn natürlich teilweise auch repetitiv).

    minastaros schrieb:

    Denk dran, dass das pimpl-Objekt an einem Pointer hängt. Das heißt, es muss mit erzeugt und am Ende mitsamt dem Mutter-Objekt auch wieder zerstört werden.

    [...]

    Logisch, dass Du dann auch an Dinge wie Deep-Copy denken solltest, je nachdem, was Deine Klasse so machen soll.

    An all die Punkte habe ich gedacht, funktioniert auch einwandfrei.

    minastaros schrieb:

    Verwende also gleich einen smart pointer!

    Ist das eher eine Glaubensfrage? Ich konnte keine Argumente gegen ein stinknormales new/delete finden, außer, dass ein unique_ptr die Sache etwas vereinfacht:

    But the real question is why you want to use a smart pointer to begin with; it's very clear where the delete should occur, and there's no issue of exception safety or other to be concerned with. At most, a smart pointer will save you a line or two of code.

    [...]

    None of the usual smart pointers implement deep copy; you could implement one, of course, but it would probably still require a complete type whenever the copy occurs, which means that you'd still have to provide a user defined copy constructor and assignment operator (since they can't be inline). Given this, it's probably not worth the bother using the smart pointer.

    minastaros schrieb:

    Noch etwas weitere Literatur:
    http://herbsutter.com/gotw/_100/

    Dort steht:

    Prefer to hold the Pimpl using a unique_ptr. It’s more efficient than using a shared_ptr, and correctly expresses the intent that the Pimpl object should not be shared.

    Vielleicht habe ich sonstige Argumente auch einfach übersehen? Jedenfalls ist einfaches "Mach so, nicht so" für mich nicht ausreichend, wenn es anders auch zu funktionieren scheint ...

    @SeppJ: Auf der Seite herbsutter.com (Link von minastaros) stand die Antwort auf meine andere Frage (aus dem anderen Thread); wenn auch etwas "versteckt". Insofern hattest du recht, es steht auf einer der Seiten, die das pImpl-Idiom beschreiben (kannte die Seite nur nicht).


Anmelden zum Antworten