Boost Smart Pointer jetzt Standard C++?



  • Nexus schrieb:

    Tachyon schrieb:

    Tut es doch nicht. In beiden Fällen initialisierst Du ein Objekt bei der Konstruktion mit zwei Werten. Aus Abstraktionssicht geht es kaum besser.

    Aus technischer Sicht ginge es um einiges besser. Ich bin mir sicher, dass jene Syntax noch einige C++-Programmierer verwirren wird. Man kann Listen innerhalb {} nicht konsequent für Konstruktor-Argumente verwenden, ohne ins Messer zu laufen. Sobald nämlich ein Konstruktor std::initializer_list nimmt, hat man ein anderes Verhalten. In dieser Hinsicht hätte ich es für sinnvoller gehalten, so eine "uniforme" Regel, die eh nur in einem Teil der Fälle einsetzbar ist, gar nicht erst einzuführen.

    Gib mal ein Beispiel, wo es schädlich ist. Irgendwie leuchtet mit nicht ein, wo eine Unterscheidung zwischen initializer_list und nicht initializer_list einen Vorteil hätte. Zumal es je Regeln gibt, die potentielle Mehrdeutigkeiten auflösen.



  • Nexus schrieb:

    krümelkacker schrieb:

    unique_ptr unterstützt explizit unvollständige Typen. Man muss nur entweder einen eigenen Deleter verwenden oder dafür sorgen, dass Destruktor, reset und Zuweisungsoperator höchstens dort instantiiert werden, wo T dann schließlich vollständig bekannt ist.

    Das heisst, man muss aber recht viel tun, um unvollständige Typen zu unterstützen?

    Nicht wirklich. Hast Du ein konkretes Beispiel parat, bei dem Du befürchtest, Du müsstest "viel tun"? Funktionszeiger kannst Du natürlich auch als Deleter verwenden. Genauso wie ein struct-Objekt, dessen operator() dort definiert wird, wo T vollständig bekannt ist.



  • Tachyon schrieb:

    Gib mal ein Beispiel, wo es schädlich ist. Irgendwie leuchtet mit nicht ein, wo eine Unterscheidung zwischen initializer_list und nicht initializer_list einen Vorteil hätte. Zumal es je Regeln gibt, die potentielle Mehrdeutigkeiten auflösen.

    Wenn eine initializer_list vorkommt, liegt die Vorstellung einer Collection nahe, besonders wenn die Elemente den gleichen Typ haben. Die Verwendung der Syntax für normale Konstruktoraufrufe kann in die Irre führen. Beispiel:

    QVector<int> v {7, 3}; // erzeugt 7 Elemente, obwohl das
    // wie eine Initialisierungsliste mit 2 Elementen aussieht
    

    Und es kommt noch besser, falls QVector in Zukunft einen Konstruktor für std::initializer_list bekommt:

    QVector<int> v {7, 3}; // erzeugt nun plötzlich 2 Elemente!
    

    Der User-Code geht kaputt, aber kompiliert gleichzeitig mit einer ganz anderen Semantik weiter. Solche Logikfehler sind etwas vom Schlimmsten, was überhaupt passieren kann. An die denkt man nämlich immer zuletzt, wenn überhaupt. Die Mehrdeutigkeit könnte man verhindern, würde man die Initialisierungs-Syntax im Mass einsetzen. Übersehe ich hier was ganz Fundamentales oder ist diese Problematik im Standardisierungskomitee wirklich niemandem aufgefallen?

    Abgesehen davon bin ich kein Anhänger der Einstellung "schadet nichts, nettes Feature". Ich sehe den grossen Nutzen der Schreibweisen 5.-7. aus meinem vorherigen Post nicht.

    krümelkacker schrieb:

    Nicht wirklich. Hast Du ein konkretes Beispiel parat, bei dem Du befürchtest, Du müsstest "viel tun"?

    Was muss man denn auf User-Seite tun, um z.B. sowas zu realisieren? Was muss man für X und x einsetzen, und gibt es noch mehr zu erledigen?

    // --- MyClass.hpp ------------------------
    #include <memory>
    
    class Incomplete;
    
    struct MyClass
    {
        MyClass();
        std::unique_ptr<Incomplete, X> p;
    };
    
    // --- MyClass.cpp ------------------------
    #include "MyClass.hpp"
    
    class Incomplete {};
    
    MyClass::MyClass() : p(new Incomplete, x) {}
    


  • Nexus schrieb:

    [...] Der User-Code geht kaputt, aber kompiliert gleichzeitig mit einer ganz anderen Semantik weiter. Solche Logikfehler sind etwas vom Schlimmsten, was überhaupt passieren kann. [...]

    Warum sollte jemand für eine Container-Klasse die {}-Syntax nutzen, obwohl er den Konstruktor(size_t,T) benutzen will? Bei Deinem anderen Beispiel macht das auch noch Sinn, std::complex, kann man als Behälter von Real- und Imaginärteil betrachten. Da finde ich die {}-Syntax in Ordnung. Ich sehe das jetzt alles nicht so dramatisch wie Du. Es ist eher eine Frage des Klassendesigns.

    Nexus schrieb:

    krümelkacker schrieb:

    Nicht wirklich. Hast Du ein konkretes Beispiel parat, bei dem Du befürchtest, Du müsstest "viel tun"?

    Was muss man denn auf User-Seite tun, um z.B. sowas zu realisieren? Was muss man für X und x einsetzen, und gibt es noch mehr zu erledigen?

    // --- MyClass.hpp ------------------------
    #include <memory>
    
    class Incomplete;
    
    struct MyClass
    {
        MyClass();
        std::unique_ptr<Incomplete, X> p;
    };
    
    // --- MyClass.cpp ------------------------
    #include "MyClass.hpp"
    
    class Incomplete {};
    
    MyClass::MyClass() : p(new Incomplete, x) {}
    

    Da Du hier p als öffentlich deklarierst und jemand per std::move den Zeiger irgendwo anders hinverschieben kann oder einfach reset aufrufen kann, brauchst Du hier einen eigenen Deleter, was sonst ggf nicht nötig gewesen wär.

    // --- MyClass.hpp ------------------------
    #include <memory>
    
    class Incomplete;
    
    struct IDel {void operator()(Incomplete*) const;};
    
    struct MyClass
    {
        MyClass();
        std::unique_ptr<Incomplete,IDel> p;
    };
    
    // --- MyClass.cpp ------------------------
    #include "MyClass.hpp"
    
    class Incomplete {};
    
    void IDel::operator()(Incomplete*ptr) const {delete ptr;}
    
    MyClass::MyClass() : p(new Incomplete) {}
    

    Da hier der Benutzer keinen Kopierkonstruktor, Zuweisungsoperator oder Destruktor für MyClass definiert hat und weil unique_ptr move-bar ist, bekommt MyClass automatisch vom Compiler einen Move-Ctor und einen Move-Assignment-Operator spendiert (nach den neuen Regeln bzgl Compilergenerierte Kopier/Move-Operationen).



  • krümelkacker schrieb:

    Warum sollte jemand für eine Container-Klasse die {}-Syntax nutzen, obwohl er den Konstruktor(size_t,T) benutzen will?

    Ich dachte, der Grund, warum man nun für fast jede Initialisierung {} verwenden kann, liegt in der Einheitlichkeit? (Auch wenn diese mehr verspricht als sie hält, scheint mir das noch das einzige Argument zu sein.)

    Oder wieso kann man die Schreibweise mit {} überhaupt benutzen, wenn keine std::initializer_list und kein Aggregat im Spiel ist? Die abwechselnde Verwendung von () und {}, abhängig davon, ob es sich um einen Container handelt oder nicht, ist ja auch nicht gerade einheitlich. Überhaupt scheint eine uniforme Syntax nicht möglich zu sein, aber am nähesten kommt man ihr meiner Meinung nach mit den Schreibweisen 1. bis 4. (vorletzter Post von mir). Der Versuch, die Fallunterscheidung mit ausschliesslicher Verwendung von {} einzudämmen, artet lediglich in noch komplizierteren Regeln und hinterlistigen Fehlerquellen aus.

    Zusammengefasst: Was spricht überhaupt für die Schreibweisen 5. bis 7.? Zur Rückgabe ohne Typangabe hätte man auch sowas machen können:

    return auto(arg1, arg2);
    

    krümelkacker schrieb:

    Da Du hier p als öffentlich deklarierst und jemand per std::move den Zeiger irgendwo anders hinverschieben kann oder einfach reset aufrufen kann, brauchst Du hier einen eigenen Deleter, was sonst ggf nicht nötig gewesen wär.

    Sorry, das war nur der Einfachheit halber. Was würde ein privates p ändern?

    Du hast Recht, der Aufwand hält sich in Grenzen (wobei du auch alles auf zwei Zeilen gequetscht hast :p). Doch jedes Mal diesen Boilerplate-Code zu schreiben kann nervig sein. In einem Template auslagern geht auch nicht gut, weil dann wieder der Typ bekannt sein müsste. Man könnte eventuell Type Erasure verwenden...



  • krümelkacker schrieb:

    unique_ptr<int[]> p (new int[123]);
      unique_ptr<doube> q (new double);
    

    Wie kann man denn im Template solche Unterscheidungen machen?
    Also wie kann ich differenzieren, ob ich ein Array reinkriege oder nicht
    und mich dementsprechend anders verhalten? (delete vs. delete[])



  • Nachgefragt... schrieb:

    krümelkacker schrieb:

    unique_ptr<int[]> p (new int[123]);
      unique_ptr<doube> q (new double);
    

    Wie kann man denn im Template solche Unterscheidungen machen?
    Also wie kann ich differenzieren, ob ich ein Array reinkriege oder nicht
    und mich dementsprechend anders verhalten? (delete vs. delete[])

    So:

    template <class T>
    struct test
    {
    	static const int value = 0;
    };
    
    template <class T>
    struct test<T []>
    {
    	static const int value = 1;
    };
    
    static_assert(test<int>::value == 0);
    static_assert(test<int[]>::value == 1);
    


  • Nexus schrieb:

    krümelkacker schrieb:

    Da Du hier p als öffentlich deklarierst und jemand per std::move den Zeiger irgendwo anders hinverschieben kann oder einfach reset aufrufen kann, brauchst Du hier einen eigenen Deleter, was sonst ggf nicht nötig gewesen wär.

    Sorry, das war nur der Einfachheit halber. Was würde ein privates p ändern?

    Damit könntest Du als Klassendesigner garantieren, dass die Elementfunktionen von unique_ptr, die eventuell zu einer Freigabe führen (Zuweisung, reset, Destruktor), nur dort instantiiert werden, wo Incomplete vollständig bekannt ist. Dann brauchst Du keinen eigenen Deleter mehr. Aber kürzer wird es damit nicht, weil Du eben noch Destruktor, Move-Ctor und Move-Zuweisung der MyClass-Klasse innerhalb von myclass.cpp definieren musst:

    // header
    class Incomplete;
    
    class MyClass {
    public:
      MyClass();
      ~MyClass();
      MyClass(MyClass&&);
      MyClass& operator=(MyClass&&) &;
    private:
      unique_ptr<Incomplete> ptr_;
    };
    
    // *.cpp
    class Incomplete {};
    
    MyClass::MyClass() : ptr_(new Incomplete) {}
    
    MyClass::~MyClass() = default;
    MyClass::MyClass(MyClass&&) = default;
    MyClass& MyClass::operator=(MyClass&&) = default;
    

    Da das nicht wirklich kürzer ist, würde ich auch bei so etwas einen eigenen Deleter verwenden.

    Nexus schrieb:

    Du hast Recht, der Aufwand hält sich in Grenzen (wobei du auch alles auf zwei Zeilen gequetscht hast :p).

    Naja, so lang sind die ja nicht. 🙂

    Nexus schrieb:

    Doch jedes Mal diesen Boilerplate-Code zu schreiben kann nervig sein. In einem Template auslagern geht auch nicht gut, weil dann wieder der Typ bekannt sein müsste. Man könnte eventuell Type Erasure verwenden...

    Ja. Man kann es zB auch mit Funktionszeigern und Lambdas machen. Folgendes sollte funktionieren:

    // header
    class Incomplete;
    
    class MyClass {
    public:
      MyClass();
    private:
      unique_ptr<Incomplete,void(*)(Incomplete*)> ptr_;
    };
    
    // *.cpp
    class Incomplete {};
    
    MyClass::MyClass()
    : ptr_(new Incomplete,[](Incomplete*p){delete p;})
    {}
    

    Es ist noch ein bischen kürzer als die Variante mit dem IDel struct. Das unique_ptr-Objekt wird aber wahrscheinlich etwas größer wegen dem zu speichernden Funktionszeiger. Mit Deletern in Form von Klassen ohne Datenelemente ist es möglich, die vom Standard erlaubte "empty base class optimization" auszunutzen (man kann zB std::tuple<T*,Deleter> als Datenelement-Typ im unique_ptr verwenden).

    Nachgefragt... schrieb:

    krümelkacker schrieb:

    unique_ptr<int[]> p (new int[123]);
      unique_ptr<doube> q (new double);
    

    Wie kann man denn im Template solche Unterscheidungen machen?
    Also wie kann ich differenzieren, ob ich ein Array reinkriege oder nicht
    und mich dementsprechend anders verhalten? (delete vs. delete[])

    Partielle Spezialisierungen. Es gibt eine partielle Spezialisierung von default_deleter für T[], welcher dann delete[] statt delete verwendet und es gibt eine partielle Spezialisierung von unique_ptr für T[], welcher zusätzlich noch den Index-Operator anbietet aber dafür keine Derived->Base-Konvertierung mehr zulässt.



  • Okay, danke für die Erklärung! Scheint so, als würde ich bei unvollständigen Typen besser auf meine Smart-Pointers zurückgreifen. Dafür sind die noch nicht wirklich movable, und für normale RAII-Zeiger lohnt sich unique_ptr wohl auch eher.

    Weiss noch jemand was zur neuen Initialisierung? Also worin genau der Grund bestand, 6. und 7. einzuführen (soweit ich weiss, funktionierte 5. bereits in C++98 für skalare Objekte)? Auch Spekulationen sind willkommen. 🙂



  • Nexus schrieb:

    Okay, danke für die Erklärung! Scheint so, als würde ich bei unvollständigen Typen besser auf meine Smart-Pointers zurückgreifen.

    Mal doof nachgefragt: Wieso scheint das so? So, wie ich das verstanden habe, verwendest Du auch Funktionszeiger für das Löschen. Wo ist da der Unterschied zu unique_ptr<Incomplete,void(*)(Incomplete*)> ?



  • krümelkacker schrieb:

    Mal doof nachgefragt: Wieso scheint das so? So, wie ich das verstanden habe, verwendest Du auch Funktionszeiger für das Löschen. Wo ist da der Unterschied zu unique_ptr<Incomplete,void(*)(Incomplete*)> ?

    Ja, aber die Funktionszeiger sind bei mir in der Implementierung und nicht im Client-Code. Vom Mechanismus her ist es das gleiche, aber ich finde meine Smart-Pointer von der Anwendung her einfacher, wenn unvollständige Typen im Spiel sind:

    #include "SmartPtr.hpp"
    
    class Incomplete;
    
    class MyClass
    {
        public:
            MyClass();
    
        private:
            SmartPtr<Incomplete> p;
    };
    
    // --- MyClass.cpp ------------------------
    #include "MyClass.hpp"
    
    class Incomplete {};
    
    MyClass::MyClass() : p(new Incomplete) {}
    

    Also kommt man als Benutzer nie mit Funktionszeigern in Berührung, man muss auch die Grossen Drei nicht überladen. Der Funktionszeiger wird im Konstruktor von SmartPtr zugewiesen (wo ja der Typ wegen new ohnehin bekannt ist).



  • @Nexus:
    C++0x Pimpl reloaded:

    upi.hpp

    #ifndef UPI_HPP_INCLUDED
    #define UPI_HPP_INCLUDED
    
    #include <memory>
    #include <utility>
    #include "boost/checked_delete.hpp"
    
    // upi = unique pointer to incomplete
    
    template<class T>
    using upi = std::unique_ptr<T,void(*)(T*)>;
    
    template<class T>
    void upi_deleter_func(T*ptr)
    {
      boost::checked_delete(ptr);
    }
    
    template<class T, class...Args>
    upi<T> make_upi(Args&&...args)
    {
      return {
        new T(std::forward<Args>(args)...),
        upi_deleter_func<T>
      };
    }
    
    #endif//UPI_HPP_INCLUDED
    

    myclass.hpp

    #ifndef MYCLASS_HPP_INCLUDED
    #define MYCLASS_HPP_INCLUDED
    
    #include "upi.hpp"
    
    class Incomplete;
    
    class MyClass
    {
    public:
      MyClass();
    private:
      upi<Incomplete> ptr_;
    };
    
    #endif//MYCLASS_HPP_INCLUDED
    

    myclass.cpp

    #include "myclass.hpp"
    
    class Incomplete {};
    
    MyClass::MyClass() : ptr_(make_upi<Incomplete>()) {}
    


  • Okay, das wäre eine Idee. Ich hätte jetzt eher an was Konventionelles gedacht (aber ohne eine konkrete Implementierung im Kopf zu haben), ich bin mit den C++0x-Features auch noch zu wenig vertraut.

    Aber trotzdem ist es natürlich leicht umständlicher, wenn man nicht das normale new verwenden kann. Bis Variadic Templates von Visual Studio unterstützt werden, wirds wohl auch noch eine Weile gehen... Ich werd wohl vorerst bei meinen Smart-Pointern bleiben, auch um C++98-kompatibel zu bleiben (die kommen nämlich in eine Bibliothek). Hoffentlich bist du nicht enttäuscht 🙂

    Dennoch vielen Dank für den Vorschlag, so habe ich wieder was gelernt. Auch das using scheint wirklich enorm nützlich zu sein 😉

    Edit: Deinen Edit gelesen



  • Nexus schrieb:

    [...] Hoffentlich bist du nicht enttäuscht 🙂

    Och nee. Ich hab' ja auch was gelernt (bzgl Deiner SmartPtr-Klasse). 🙂



  • krümelkacker schrieb:

    // header
    class Incomplete;
    
    class MyClass {
    public:
      MyClass();
    private:
      unique_ptr<Incomplete,void(*)(Incomplete*)> ptr_;
    };
    
    // *.cpp
    class Incomplete {};
    
    MyClass::MyClass()
    : ptr_(new Incomplete,[](Incomplete*p){delete p;})
    {}
    

    Dir ist aber schon klar, dass das nicht standardkonform ist? Eine Lambda-Funktion hat keinen definierten Typ und lässt sich nur beim GCC in einem Funktionszeiger speichern.



  • 314159265358979 schrieb:

    Dir ist aber schon klar, dass das nicht standardkonform ist? Eine Lambda-Funktion hat keinen definierten Typ und lässt sich nur beim GCC in einem Funktionszeiger speichern.

    Siehe n3126.pdf, 5.1.2 [expr.prim.lambda], Absatz 6

    The closure type for a lambda-expression with no lambda-capture has a public non-virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type’s function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type’s function call operator.



  • Okay, wieder was dazu gelernt 🙂


Anmelden zum Antworten