Frage zu deletern



  • Nexus schrieb:

    krümelkacker schrieb:

    Meinst Du bei std::unique_ptr? Falls ja: Das ist so am flexibelsten.

    Mit der gleichen Begründung könnte man allerdings auch die Gegenseite vertreten 😉

    Nein, wieso? Was ist denn an der Beschränkung auf Funtionszeiger so toll? Du erzwingst damit eine Indirektion. Der Deleter-Code kann nicht mehr ge-inline-t werden und sizeof(unique_ptr<T,void()(T)>) wird dann eher sizeof(void*)+sizeof(void(*)()) sein wohingegen ein zustandsloser Funktor per EBCO (empty base class optimization) auch platztechnisch keinerlei Overhead bedeutet. Wenn Du Dich auf Funktionszeiger beschränkst, erzwingst Du Overhead, den Du nicht immer gebrauchen kannst. Auch gestaltet sich die Konvertierung von Zeigern (derived -> base) als wenigstens "schwierig" bei Funktionszeiger-Deletern, wohingegen unique_ptr mit dem Defaultdeleter in der Hinsicht keine Probleme macht.

    Ich verstehe absolut nicht, was Du gegen das unique_ptr-Design hast. Dadurch, dass der Deleter-Typ ein Templateparameter ist, kannst Du doch weiterhin Funktionszeiger benutzen, wenn Du willst. Und wenn nicht, dann eben einen anderen Funktortypen.



  • Nexus schrieb:

    und kann zur Laufzeit geändert werden

    Dir ist klar, dass auch Klassen einen Funktionszeiger speichern (und ändern) können und die Lösung von Klassen eine Übermenge zu der Zeigerlösung ist?

    Vielleicht spart sie sich in wenigen Ausnahmefällen etwas Schreibarbeit, aber noch viel mehr Fälle sind quasi unmöglich.



  • EOutOfResources schrieb:

    Wieso werden deleter als Typ und nicht als Funktionszeiger übergeben?

    Abgesehen von genannten Gründen, würde mich das stark limitieren, da ich auch keine Objekte mehr übergeben könnte. Zum Beispiel:

    template<typename T>
    struct deleter {
        void operator() (T* p) {
            delete p;
        } 
    };
    


  • krümelkacker schrieb:

    Nein, wieso? Was ist denn an der Beschränkung auf Funtionszeiger so toll?

    Nichts, aber die hat man ja auch nicht. boost::shared_ptr kann beliebige Funktionsobjekte als Deleter nehmen.

    krümelkacker schrieb:

    Du erzwingst damit eine Indirektion.

    Welche weniger performant, aber flexibler ist. Die Indirektion ist einerseits Compiler-Firewall, andererseits erlaubt sie ein Austauschen zur Laufzeit und die Interaktion von Smart-Pointern mit verschiedenen Deletern. Ausserdem ist der Code normalerweise kürzer, als wenn man gleiches mit statischen Template-Parametern erreichen will.

    krümelkacker schrieb:

    Ich verstehe absolut nicht, was Du gegen das unique_ptr-Design hast.

    Gegen das Design habe ich nichts, im Gegenteil. Für einen allgemeingültigen RAII-Pointer macht unique_ptr seine Sache sehr gut. Aber für spezielle Anliegen soll er sich auch eingestehen, dass andere Smart-Pointer-Typen besser geeignet sind. 🙂



  • Etwas ausführlicher:

    Ich halte zu viel Generizität für ein Problem. Andrei Alexandrescu zeigt das mit seinem SmartPtr -Template noch viel extremer auf. Schlussendlich kann man zwar alles irgendwie hinbiegen, doch man hat ein Biest, das verschiedenste Semantiken vereint. Beim Versuch, selbst Erweiterungen zu schreiben, merkt man auch relativ schnell, dass das Ganze doch nicht so flexibel ist und man schnell an Grenzen stösst. Gewisse Konzepte lassen sich schlecht eingliedern, oder führen zu massiv Boilerplate-Code. Das hat mich auch veranlasst, einige Smart-Pointer mit klarer, spezialisierter Semantik selbst zu implementieren, was sich sehr positiv auf deren Anwendung auswirkt.

    Interessanterweise ist so ein Design im Bezug auf objektorientierte Programmierung als Anti-Pattern verpönt (Gott-Klasse), aber bei Templates stört es niemanden. Weil halt die einzelnen Teile weniger gekoppelt sind und man keinen Overhead durch zusätzliche Funktionalität hat. Die übliche "Du musst ja nicht alles verwenden"-Philosophie. Aber betrachtet man Alexandrescus SmartPtr mit all seinen Policies als zusammengehörige Funktionalität, kann die Komplexität durchaus unvorteilhaft sein.

    Zurück zum Thema: Nur weil unique_ptr im Prinzip sehr vieles könnte, muss man ihn dafür nicht auch einsetzen. Alternativen sind manchmal die bessere Wahl.



  • Nexus schrieb:

    krümelkacker schrieb:

    Du erzwingst damit eine Indirektion.

    Welche weniger performant, aber flexibler ist. Die Indirektion ist einerseits Compiler-Firewall,

    Das ist doch Quark. Du willst mir sagen, dass eine Beschränkung auf Funktionszeiger flexibler sei, als das Ersetzen des Funktionszeigertypen durch einen Templateparameter, der immer noch ein Funktionszeigertyp sein kann.

    Nexus schrieb:

    andererseits erlaubt sie ein Austauschen zur Laufzeit und die Interaktion von Smart-Pointern mit verschiedenen Deletern. Ausserdem ist der Code normalerweise kürzer, als wenn man gleiches mit statischen Template-Parametern erreichen will.

    template<class T>
    using nexus_ptr = std::unique_ptr<T,void(*)(T*)>;
    

    Gut, unique_ptr lässt sich den Deleter nicht nachträglich manipulieren. Das hat aber nichts damit zu tun, dass dafür ein Template-Typparameter verwendet wird.



  • krümelkacker schrieb:

    Das ist doch Quark. Du willst mir sagen, dass eine Beschränkung auf Funktionszeiger flexibler sei

    Nein. Du hast mit der "Beschränkung auf Funktionszeiger" angefangen, obwohl ich dies gar nie erwähnt habe. Wovon ich sprach, ist Type Erasure wie beim Deleter von boost::shared_ptr . Okay, "flexibler" ist ein zweischneidiges Schwert. Man kann damit zwar insgesamt weniger machen, weil man mit dem statischen Ansatz die Type Erasure nachbauen kann. Aber der Nachbau wird um einiges komplizierter als das Original, bei dem die von mir angesprochene Flexibilität direkt genutzt werden kann.

    krümelkacker schrieb:

    Gut, unique_ptr lässt sich den Deleter nicht nachträglich manipulieren. Das hat aber nichts damit zu tun, dass dafür ein Template-Typparameter verwendet wird.

    Es hat aber damit zu tun, dass der Deleter-Typ fest in den Smart-Pointer-Typ eingeprägt ist. Mit dynamischer Polymorphie bist du in dieser Hinsicht flexibler. Mir ist übrigens klar, dass du über eine Indirektion etwas sehr Ähnliches auch mit unique_ptr erreichen kannst. Wir hatten die Diskussion schon einmal, damals konntest du mit mit Mühe und Not etwas basteln, das zwar nahe an meinen spezialisierten Smart-Pointer kam, aber eben nicht ganz :p

    Und sobald man sich eine andere Besitzsemantik einrichten will, nützt einem unique_ptr auch nichts mehr. Nur mit unique_ptr und shared_ptr hat man noch nicht alle Fälle abgedeckt. Jedoch recht viele.



  • Nexus schrieb:

    Nur mit unique_ptr und shared_ptr hat man noch nicht alle Fälle abgedeckt. Jedoch recht viele.

    ...behaupte ich auch nicht. Es ist ja nicht mal geklärt, um was für Deleter es sich in diesem Thread handelt. Ich hatte mit unique_ptr ja nur geraten. Du sagtest dazu folgendes:

    Nexus schrieb:

    krümelkacker schrieb:

    Meinst Du bei std::unique_ptr? Falls ja: Das ist so am flexibelsten.

    Mit der gleichen Begründung könnte man allerdings auch die Gegenseite vertreten 😉

    Das sieht für mich immer noch nach der Behauptung aus, dass

    template<class T>
    class unique_ptr {
      T* ptr;
      void (*del)(T*);
    public:
      explicit unique_ptr(T*);
      explicit unique_ptr(T*,void(*)(T*));
      ...
    };
    

    flexibler sei als

    template<class T, class D = default_deleter<T> >
    class unique_ptr {
      tuple<T*,D> data;
    public:
      explicit unique_ptr(T*, D = D());
      ...
    };
    

    Mag sein, dass Du es anders gemeint hast. Aber so habe ich Deinen Kommentar verstanden. Und natürlich stimmt das so nicht, da Du immernoch für D void()(T) einsetzen kannst.

    kk



  • Nexus schrieb:

    Wenn der Deleter nicht in den Typ einfliesst, muss er nicht bei der Instanziierung des unique_ptr -Templates bekannt sein und kann zur Laufzeit geändert werden (z.B. durch operator= ). Also wie bei shared_ptr . Dafür hat auf diese Weise man weniger Typsicherheit.

    Der Grund den Deleter-Typ mit in den unique_ptr reinzuknoten ist einfach damit unique_ptr so effizient wie möglich bleibt.
    Type-Erasure würde hier erfordern dass wie bei shared_ptr ein Hilfsobjekt dynamisch angelegt werden muss.
    shared_ptr braucht das sowieso, also ist es da egal. unique_ptr braucht es nicht, also auch keine Type-Erasure für den deleter.

    ps:
    wenn dann bräuchte man IMO mindestens void(*)(T*, void*) .
    Damit könnte man so ziemlich alles abdecken, bräuchte aber trotzdem keinen zusätzlichen Template-Parameter.



  • Ja, wie angedeutet war "flexibler" vielleicht etwas ungünstig formuliert, da man mit dem momentanen Design auch Type Erasure weitgehend nachbauen kann.

    Das Problem ist aber, dass der Aufwand für diesen Nachbau relativ hoch ist und für eine vernünftige Anwendung auch eher fortgeschrittene Kenntnisse (teilweise aus C++0x) erfordert. Daher denke ich, bei der Notwendigkeit einer dynamischeren Technik wird der durchschnittliche Programmierer diese Möglichkeit nicht nutzen, sondern eher zu shared_ptr greifen, wo die Deleter mit dynamischer Polymorphie bereits fertig implementiert sind.

    Natürlich hat der unique_ptr mit seinem momentanen Design viele Vorteile, da man grundsätzlich mehr damit machen kann und keine Performance verliert. Wie schon mehrfach erwähnt bin ich damit zufrieden. Aber man sollte auch sehen, dass das momentane Design grösstenteils auf die naheliegendste Art benutzt werden wird (über einen Funktor als Deleter), während speziellere Anforderungen mit anderen Smart-Pointer-Typen massiv einfacher gelöst werden können.


Anmelden zum Antworten