Private-Member angeben



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



  • minastaros schrieb:

    PS: es muss sogar sein! Herb Sutter erklärt in dem Artikel (link oben) auch, warum.

    Ist das nicht noch mehr ein Argument für new/delete? Schließlich muss ich dann eh einen (Kopier-)Konstruktor und Destruktor anlegen, da ja sonst gar kein Speicher alloziert wird und das Programm gar nicht erst richtig läuft.



  • Über den Sinn von unique_ptr in einer Klasse kann man sich streiten. Wenn das delete im Destruktor steht wird der Speicher auch garantiert wieder freigegeben. Allerdings sind schon zwei Pointer in einer Klasse die mit new reserviert werden nicht mehr trivial. Bei lokalen Variablen sollte man new/delete auf jeden Fall vermeiden und unique_ptr vorziehen, da es viel zu viele Möglichkeiten gibt das delete zu vergessen.



  • Private schrieb:

    minastaros schrieb:

    PS: es muss sogar sein! Herb Sutter erklärt in dem Artikel (link oben) auch, warum.

    Ist das nicht noch mehr ein Argument für new/delete? Schließlich muss ich dann eh einen (Kopier-)Konstruktor und Destruktor anlegen, da ja sonst gar kein Speicher alloziert wird und das Programm gar nicht erst richtig läuft.

    Nicht wirklich ein Argument dafür.

    Wenn Smart Pointer grundsätzlich für verwaltete Resourcen verwendet werden, ergibt das eine gewisse Konsistenz und Wiedererkennbarkeit, was ihre Aufgabe betrifft. Damit unterscheiden sie sich auch optisch sofort von den "normalen" Pointern, bei denen es dann offensichtlich nicht um Resourcenverwaltung geht.

    Mag man so oder so sehen. Eher eine Stil-Frage, wobei sich die "Gurus" allesamt dafür aussprechen. Bei konsequenter Anwendung von Smart-Pointern wird das Leben einfach insgesamt sicherer und entspannter.


Anmelden zum Antworten