Programiersprache für Anfänger



  • sothis_ schrieb:

    Ist aber wertlos, weil du ja keinen Zugriff auf die privaten Variablen hast.

    die man dann aber mit einem 'this' versorgen muss

    :xmas1:

    Der Zeiger bringt dir doch nix, weil du keinen Zugriff aus der freien (also nicht-Member-) Funktion auf private Member-Variablen hast.



  • Badestrand schrieb:

    Der Zeiger bringt dir doch nix, weil du keinen Zugriff aus der freien (also nicht-Member-) Funktion auf private Member-Variablen hast.

    c++ kennt doch diverse tricks, ich sage nur: 'C++: where friends have access to your private members'
    🙂



  • ~fricky schrieb:

    c++ kennt doch diverse tricks, ich sage nur: 'C++: where friends have access to your private members'

    Dann musst du aber wieder alle Hilfsfunktionen in der Klassendeklaration auflisten (eben als friends), genau das sollte doch vermieden werden.



  • Badestrand schrieb:

    ~fricky schrieb:

    c++ kennt doch diverse tricks, ich sage nur: 'C++: where friends have access to your private members'

    Dann musst du aber wieder alle Hilfsfunktionen in der Klassendeklaration auflisten (eben als friends), genau das sollte doch vermieden werden.

    okay, dann war das ein mist-vorschlag. vergesst ihn einfach.
    🙂



  • Bashar schrieb:

    Alles mit void*. Wahnsinnig sauber.

    Das ist ja kein grundsätzliches Problem; siehe DECLARE_HANDLE().



  • Man will in der OOP Subtyping haben, d.h. Objekte einer abgeleiteten Klasse sollen dort eingesetzt werden können, wo eine Basisklasse erwartet wird. Mit void* kann man überall Objekte jeden Typs einsetzen, das ist zu allgemein. DECLARE_HANDLE kapselt m.W. void* in eigene Strukturen(?), so dass die Typen alle disjunkt sind, das Subtyping geht also verloren.



  • naja OOP is ja nicht das einzige an C++ was 'schön' ist.
    mach mal template metaprogrammierung mit C (ohne makros natürlich!)



  • Bashar schrieb:

    DECLARE_HANDLE kapselt m.W. void* in eigene Strukturen(?), so dass die Typen alle disjunkt sind, das Subtyping geht also verloren.

    Ja, das ist natürlich richtig - und bereits ein guter Grund, das C++-Typsystem zu verwenden. Mein Punkt war, daß es durchaus auch typsicher sein kann.

    (Sogar so etwas wie typsichere Vererbung könnte man so simulieren:

    #define DECLARE_HANDLE(name) \
        typedef struct tag##name {}* name;
    #define DECLARE_HANDLE_INHERITED(name, _base) \
        typedef struct tag##name { _base base; } name;
    

    Aber spätestens da sollte man der Wartbarkeit zuliebe C++ verwenden.)



  • DEvent schrieb:

    Was hat bitte RAII mit Resourcen-Verwaltung zu tun? Im Destruktor sollte man keine externe Resourcen freigeben, denn Destruktor kann nichts im Fehlerfall machen.

    Destruktoren sollten so gestaltet sein, daß sie niemals fehlschlagen und das Programm in einem definierten Zustand belassen. Es ist in C++ absolut unsinnig einen Destruktor einen Exception werfen zu lassen, daß dies beim Stack Unwinding dann logischerweise zum Werfer einer Exception in einem weiteren Destruktor führen dürfte, und dies instantan mit unexspected() endete.
    Fazit: In C++ kann man seine Destruktoren gleich "throw()" definieren, da alles andere keinen Sinn ergibt.

    DEvent schrieb:

    Eine einfache close()-Methode reicht aus.

    filehandler = new Handler();
    // ...
    filehandler.close();
    

    Dein Code führt direkt ins Resourcenleck! Denn wenn im "..." eine Exception geworfen wird, dann wird close niemals ausgeführt!
    Daher müßte das analog zu unten formuliert sein.

    filehander = new Handler ();
    try {
        // Code, der eine Exception werfen kann
    }
    finally {
        Handler.close();
    }
    

    Daher ist eben das "finally" Statement so wichtig. Bei einer Sprache mit RAII müssen dagegen die Destruktoren entsprechend gestaltet sein. Das führt uns dann zu folgendem Code

    {
        Handler filehandler ();
    
        // Code, der eine Exception werfen kann
    } // <- filehandler wird hier zerstört, egal ob eine Exception geworfen wurde oder nicht
    

    Hier brauche ich kein explizites finally Statement, damit der Handler sauber abgeräumt wird. Es ist lediglich erforderlich, daß der Destruktor fehlerfrei funktioniert.



  • ~john schrieb:

    Es ist lediglich erforderlich, daß der Destruktor fehlerfrei funktioniert.

    Wenn du externe Resourcen hast, dann kann immer was passieren. Nenn mir doch ein Beispiel, bei dem wirklich kein Fehler vorstellbar ist. (ein Socket kann nicht geschlossen werden, eine Datei nicht geschrieben/geschlosen, usw.). Deswegen muss eine close()-Methode dabei sein, die eine Exception werfen kann, weil eben ein destructor nichts werfen darf. Und deswegen ist auch RAII hier vollkommen nutzlos.

    asc schrieb:

    Dafür ist es in C dadurch beschmutzt, das jeder nach belieben auf deinen String zugreifen kann.

    Kannst du mir erklaeren, wie du das meinst?

    ~fricky schrieb:

    1. mach dir ne basisklasse mit den hilfsfunktionen. dann sieht man zwar noch, dass die klasse erbt, aber nicht die funktionen selber.

    Und dann muss ich aber immernoch jedesmal das gesammte Programm neu uebersetzen, wenn ich irgendwas in dieser Basisklasse aendere. Naja, inzwischen greift man ja zu Techniken wie "cached Header Files" (oder so aehnlich), so das nur ein Teil neu uebersetzt wird.

    ~fricky schrieb:

    2. freie funktionen mit nem 'static' davor, die man dann aber mit einem 'this' versorgen muss. dann sieht man garnix mehr von den hilfsfunktionen.

    Ja - C-Stil eben.



  • Bashar schrieb:

    Man will in der OOP Subtyping haben, d.h. Objekte einer abgeleiteten Klasse sollen dort eingesetzt werden können, wo eine Basisklasse erwartet wird. Mit void* kann man überall Objekte jeden Typs einsetzen, das ist zu allgemein. DECLARE_HANDLE kapselt m.W. void* in eigene Strukturen(?), so dass die Typen alle disjunkt sind, das Subtyping geht also verloren.

    Du meinst das coole Feature von C++:

    void fuettereDogsMitHundefutter(std::vector<Dog*> dogs)
    {
    }
    
    int main
    {
      std::vector<Pet*> pets;
      pets.push_back(new Dog());
      pets.push_back(new Dog());
      pets.push_back(new Dog());
    
      fuettereDogsMitHundefutter(pets);
    }
    


  • DEvent schrieb:

    ~john schrieb:

    Es ist lediglich erforderlich, daß der Destruktor fehlerfrei funktioniert.

    Wenn du externe Resourcen hast, dann kann immer was passieren. Nenn mir doch ein Beispiel, bei dem wirklich kein Fehler vorstellbar ist. (ein Socket kann nicht geschlossen werden, eine Datei nicht geschrieben/geschlosen, usw.). Deswegen muss eine close()-Methode dabei sein, die eine Exception werfen kann, weil eben ein destructor nichts werfen darf. Und deswegen ist auch RAII hier vollkommen nutzlos.

    Und wie gibst du dann in einem Fehlerfall deine Ressourcen so frei, dass du weitere Fehler vernünftig trackst und keine Lücken in dein System reißt?



  • Und wie gibst du dann in einem Fehlerfall deine Ressourcen so frei, dass du weitere Fehler vernünftig trackst und keine Lücken in dein System reißt?

    Das ist eine gute Frage. Allerdings ganz egal wie die Antwort aussieht...

    Netterweise kann man so ziemlich alles was man hier ohne Destruktoren und/oder Exceptions machen kann auch MIT Destruktoren und Exceptions machen (und IMO einfacher).
    Wenn man z.B. das File-Handle, in dem Fall wo man es nicht schliessen kann, in eine Liste stecken möchte (um dann später was auch immer damit zu machen), dann kann man das genausogut im Destruktor machen.

    Wenn man den Fall explizit behandeln möchte kann man der Klasse eine Close Funktion spendieren, die dann per Konvention aufgerufen werden muss bevor man ein Objekt "sterben" lässt. Im Destruktor kann man dann netterweise prüfen ob die Konvention eingehalten wurde (Debug Build), oder einen "best effort" machen die Situation noch irgendwie hinzubiegen (Release Build). Oder, bei absolut kritischen Projekten, kann man sich darauf beschränken das Programm in so einem Fall einfach mit einem Fehler abzubrechen. Der automatische Aufruf von Destruktoren ermöglicht es einem wenigstens viele dieser Fälle sehr schnell zu finden. In C ist ein vergessener "Close" Aufruf einfach ein unbehandeltes Resource-Leak und aus. Und selbst wenn man dieses Verhalten in C++ aus irgendeinem Grund braucht oder haben möchte lässt sich das machen -- wobei ich jetzt keinen Fall wüsste wo ich das für Sinnvoll halten würde.

    ----

    Allerdings muss ich sagen dass ich noch nie einen Fall hatte wo ich einen Socket (mit einem gültigen Descriptor) nicht schliessen hätte können, oder ein File-Handle, oder sonstwas in der Art. Für das OS ist es ein leichtes diese Operationen so zu implementieren dass sie nie fehlschlagen können.

    Andere Situationen, in denen eben nicht garantiert werden kann dass das Close() in allen Fällen funktioniert (z.B. wenn Close() ein Flush() impliziert, oder man noch einen End-Tag in ein File schreiben möchte oder was auch immer) muss man anders behandeln. Hier hat man aber auch grundsätzlich die gleichen Möglichkeiten mit oder ohne RAII, nur die Implementierung sieht im Detail etwas anders aus.

    Das ganze Geraunze dass RAII eine schlechte Idee wäre weil man im dtor nicht sinnvoll Exceptions werfen kann zeugt IMO nur davon dass jmd. die ganze Sache nicht verstanden hat.

    Für wahrscheinlich 99% aller Applikationen ist ein einfaches "räum auf, und ignorier falls was schief geht" im dtor vollkommen ausreichend. Für die Fälle wo das nicht reicht muss man eben wie gesagt etwas mehr Code schreiben, aber zu behaupten dass RAII einem dabei nicht hilft wäre wie zu behaupten dass ein blinder keinen Stock gebrauchen kann.

    ----

    Die Forderung "ein dtor darf nix werfen" ist auch nix was aus irgendeinem Fehldesign heraus entsteht, sondern ein grundlegendes Problem welches man genauso antrifft wenn man ohne Destruktoren und Exceptions programmiert. Was soll man denn in C/asm/... in der "HandleErrorXYZ" Funktion machen wenn einem z.B. der Speicher ausgeht um irgendwas zu loggen, in eine Liste einzutragen oder was auch immer? Man muss eben beschliessen solche Fälle entweder zu ignorieren, oder aber das Programm ganz abzubrechen, oder irgendwie sicherstellen dass es nicht passieren kann (Speicher den man für solche Fälle braucht kann man z.B. beim Erstellen eines Objektes bereits anfordern etc.). Alle diese Möglichkeiten lassen sich mit oder ohne Destruktoren, mit oder ohne Exceptions implementieren. Im Endeffekt geht es nur darum dass man die unmittelbare Fehlerbehandlung irgendwann abschliessen muss, auch wenn bei der Fehlerbehandlung weitere Fehler passieren. Was aber nicht bedeutet dass man den Fehler nicht irgendwo vermerken, und sich später darum kümmern könnte.

    Destruktoren (bzw. deterministische Finalisierung/Destruktion) lösen nicht auf magische Weise das Problem "Resourcen-Verwaltung", sie sind nur ein Hilfsmittel welches einem gewisse Dinge abnimmt bzw. das Leben sehr vereinfacht, indem sie gewisse Fälle automatisieren, den benötigten Code verkürzen etc. Das Denken nehmen sie einem aber nicht ab.



  • DEvent schrieb:

    Du meinst das coole Feature von C++:

    Man kann also OOP deiner Meinung nach nicht sauber in einem statischen Typsystem betreiben? Du würdest lieber ein Array aus void* übergeben und jedes Element darauf prüfen, ob es ein Dog ist, ja?



  • hustbaer schrieb:

    Allerdings muss ich sagen dass ich noch nie einen Fall hatte wo ich einen Socket (mit einem gültigen Descriptor) nicht schliessen hätte können, oder ein File-Handle, oder sonstwas in der Art. Für das OS ist es ein leichtes diese Operationen so zu implementieren dass sie nie fehlschlagen können.

    klar, ein OS hat schon seine eigene müllabfuhr. aber für die gegenstelle, oder das speichermedium, ist es ein bedeutsamer unterschied, ob die verbindung bzw. die datei im gegenseitigen einvernehmen geschlossen, radikal abgebrochen wird, oder ob gar nichts passiert. in den beiden letzten fällen ist datenverlust sehr wahrscheinlich. daher is RAII zwar ein netter versuch, der aber, wie so vieles in C++, einfach nicht zu ende gedacht wurde.
    🙂



  • aber für die gegenstelle, oder das speichermedium, ist es ein bedeutsamer unterschied, ob die verbindung bzw. die datei im gegenseitigen einvernehmen geschlossen, radikal abgebrochen wird, oder ob gar nichts passiert. in den beiden letzten fällen ist datenverlust sehr wahrscheinlich

    Und nur erklär mir mal bitte was das mit RAII zu tun hat?



  • DEvent schrieb:

    ~john schrieb:

    Es ist lediglich erforderlich, daß der Destruktor fehlerfrei funktioniert.

    Wenn du externe Resourcen hast, dann kann immer was passieren. Nenn mir doch ein Beispiel, bei dem wirklich kein Fehler vorstellbar ist. (ein Socket kann nicht geschlossen werden, eine Datei nicht geschrieben/geschlosen, usw.). Deswegen muss eine close()-Methode dabei sein, die eine Exception werfen kann, weil eben ein destructor nichts werfen darf. Und deswegen ist auch RAII hier vollkommen nutzlos.

    Es gibt zwei Möglichkeiten mit einer close Methode.

    • Durch die Exception wird der Codeblock verlassen, ohne die close Methode auszuführen -> Resourcenleck
    • Über einen Exceptionhandler wird die Closemethode im Falle einer Exception trotzdem ausgeführt. In diesem Fall muß die Methode "close()throw()" sein. Denn wenn close eine Exception werfen darf, dann führt das instantan zum Aufruf von unexspected(). Daher kann man in einem Destruktor oder in einer close Methode gleich unexspected aufrufen, es ändert am Ergebnis nichts. Viel mehr sollte man sich Gedanken machen, wie man über die richtige Lebensdauer eines Objekts solche Probleme vermeidet bzw. man muß sich überlegen wie man die Kuh trotzdem vom Eis holt.


  • hustbaer schrieb:

    aber für die gegenstelle, oder das speichermedium, ist es ein bedeutsamer unterschied, ob die verbindung bzw. die datei im gegenseitigen einvernehmen geschlossen, radikal abgebrochen wird, oder ob gar nichts passiert. in den beiden letzten fällen ist datenverlust sehr wahrscheinlich

    Und nur erklär mir mal bitte was das mit RAII zu tun hat?

    es hat was damit zu tun, dass c++ destruktoren nahezu unbrauchbar sind. aber falls du darauf hinaus willst, dass man RAII wörtlich nehmen sollte: 'acquisition' und 'initialisation' sagt natürlich nix darüber aus, wie man irgendwas auch wieder vernünftig schliesst und eventuell auf fehlschläge reagiert.
    🙂



  • ~fricky schrieb:

    'acquisition' und 'initialisation' sagt natürlich nix darüber aus, wie man irgendwas auch wieder vernünftig schliesst und eventuell auf fehlschläge reagiert.

    Schlag doch mal vor, wie eine Software bei einem Fehlerfall im Sinne von "kann Datei nicht schließen" reagieren sollte/könnte.



  • ~fricky schrieb:

    es hat was damit zu tun, dass c++ destruktoren nahezu unbrauchbar sind.

    In dem Fall wo die C++ Destruktoren versagen ist auch C am Ende. Und viele Probleme an denen C krankt lassen sich deutlich konfortabler mittels Destruktoren lösen. Ja, C++ ist nicht der Heilige Gral, aber C mit Sicherheit ebenso wenig.

    Zum Rest: Sie Post von Badestrand...


Anmelden zum Antworten