Boost Smart Pointer jetzt Standard C++?



  • Kann mir jemand einen Link geben wo ich gute Informationen zu C++0x bekomme?



  • icarus2 schrieb:

    Kann mir jemand einen Link geben wo ich gute Informationen zu C++0x bekomme?

    Dort ist alles wichtige zusammengefasst:
    http://www2.research.att.com/~bs/C++0xFAQ.html



  • Athar schrieb:

    icarus2 schrieb:

    Kann mir jemand einen Link geben wo ich gute Informationen zu C++0x bekomme?

    Dort ist alles wichtige zusammengefasst:
    http://www2.research.att.com/~bs/C++0xFAQ.html

    Danke dir. Werde morgen mal anfangen zu lesen 🙂



  • C++0x Enttäuschter schrieb:

    template<class T, class...Args>
    unique_ptr<T> make_unique(Args&&...args)
    {
      unique_ptr<T> result (new T(std::forward<Args>(args)...));
      return result;
    }
    

    Respekt. Die haben es echt geschafft eine eh schon sehr komplexe Sprache noch unleserlicher zu machen.

    Als DAU wirst Du solchen Code kaum sehen müssen. Viele der neuen "core language features" sind hauptsächlich dazu da, die Werkzeugkiste von Entwicklern von generischen Bibliotheken zu erweitern, so dass diverse Bibliotheks-Features elegant implementiert werden können. Im obigen Beispiel sind die Stichwörter "variadic templates" und "perfect forwarding". Perfect forwarding braucht man zB bei std::function und std::bind. Variadic templates ebenso. Du willst nicht wissen, was für ein Aufwand nötig ist, std::bind ohne diese Features zu implementieren/approximieren.

    Einige der Spracherweiterungen erhöhen auch die Uniformität, was bedeutet, dass es weniger Sonderregeln geben wird. Zumindest war das die Strategie um C++0x für Anfänger leichter zu machen. Als Beispiel wird hier gerne B.S. zu "uniform initialization" zitiert:

    vector<int> primes = {2,3,5,7,11};
    


  • C++0x Enttäuschter schrieb:

    template<class T, class...Args>
    unique_ptr<T> make_unique(Args&&...args)
    {
      unique_ptr<T> result (new T(std::forward<Args>(args)...));
      return result;
    }
    

    Respekt. Die haben es echt geschafft eine eh schon sehr komplexe Sprache noch unleserlicher zu machen.

    👎

    Im Vergleich zum üblichen STL Code ist es sehr lesbar 🙂



  • krümelkacker schrieb:

    Ich finde, mit dem, was die C++0x Lib bietet bzgl schlauen Zeigern, kann man eigentlich zufrieden sein.

    Ja, unique_ptr ist recht cool. Mir reicht das aber nicht immer, deshalb hab ich noch meine eigene SmartPointer-Werzeugkiste. Zum Beispiel gibt es fast nirgends vernünftige Implementierungen von kopierenden Smart-Pointern.

    Mir ist manchmal auch wichtig, dass der Pointee-Typ bei der Deklaration nicht bekannt sein muss. shared_ptr kann das, trifft das auf unique_ptr auch zu?

    krümelkacker schrieb:

    Einige der Spracherweiterungen erhöhen auch die Uniformität, was bedeutet, dass es weniger Sonderregeln geben wird.

    Das ist einer der Punkte, die mich an C++0x am meisten stören: Diese "Uniformität", nun 4 Schreibweisen für das Gleiche zu haben.

    int a = 4;
    int a(4);
    int a = {4};
    int a {4};
    

    Mindestens auf die letzte hätte man meiner Meinung nach ganz verzichten können. Und besonders mühsam wirds, wenn das Gleiche komplett Unterschiedliches bedeutet.

    std::complex<double> c {3.2, 4.5}; // Ruft den Konstruktor mit 2 Argumenten auf
    std::vector<double> v {3.2, 4.5}; // Initialisierungsliste für vector!
    

    Ich hoffe mal, ich bin immer noch auf dem aktuellen Stand. Falls das wirklich stimmt, haben sich die Leute dabei nichts überlegt? Zum Beispiel dass normaler Code mit Konstruktoraufrufe durch {} plötzlich eine ganz andere Bedeutung bekommt, wenn ein Initialisierungslisten-Konstruktor hinzugefügt wird?



  • Nexus schrieb:

    [...]Mindestens auf die letzte hätte man meiner Meinung nach ganz verzichten können. Und besonders mühsam wirds, wenn das Gleiche komplett Unterschiedliches bedeutet.

    std::complex<double> c {3.2, 4.5}; // Ruft den Konstruktor mit 2 Argumenten auf
    std::vector<double> v {3.2, 4.5}; // Initialisierungsliste für vector!
    

    [...]

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

    Was meinst Du mit kopierendem Smartpointer? Ein Smartpointer, der eine tiefe Kopie erzeugt?



  • Nexus schrieb:

    krümelkacker schrieb:

    Ich finde, mit dem, was die C++0x Lib bietet bzgl schlauen Zeigern, kann man eigentlich zufrieden sein.

    Ja, unique_ptr ist recht cool. Mir reicht das aber nicht immer, deshalb hab ich noch meine eigene SmartPointer-Werzeugkiste. Zum Beispiel gibt es fast nirgends vernünftige Implementierungen von kopierenden Smart-Pointern.

    Da habe ich auch erst kürzlich wieder darüber nachgedacht. Eigentlich könnte man das unique_ptr-Design dahingehend erweitern, dass man noch über einen "cloner" sagen kann, wie ein Objekt geklont werden soll. Der default_cloner könnte versuchen, eine clone-Funktion aufzurufen (falls vorhanden) und auf den Kopierkonstruktor zurückfallen. Beides könnte man optional halten, so dass unique_ptr weiterhin mit Typen, die nicht clone/kopierbar sind, nutzbar bleibt. Einen Ansatz dazu hatte ich auch schonmal:

    struct use_copy_ctor {};
    struct prefer_clone : use_copy_ctor {};
    
    #define AUTO_FUNC_RETURN(...) ->decltype(__VA_ARGS__) {return __VA_ARGS__;}
    
    template<class T>
    auto clone(T const* ptr, prefer_clone)
    AUTO_FUNC_RETURN(ptr->clone())
    
    template<class T>
    auto clone(T const* ptr, use_copy_ctor)
    AUTO_FUNC_RETURN(new T(*ptr))
    
    template<class T>
    struct default_cloner
    {
      T* operator()(T const* ptr) const {
        return clone(ptr,prefer_clone());
      }
    };
    

    Aber der GCC meldet im Moment noch viele Ausdrücke, die innerhalb ->decltype(...) stehen, als harten Fehler, obwohl hier eigentlich auch SFINAE greift. Mein Test mit T als abstrakter Klasse funktionierte hier noch nicht. Aber clang++ kann damit schon umgehen. 🙂

    Nexus schrieb:

    Mir ist manchmal auch wichtig, dass der Pointee-Typ bei der Deklaration nicht bekannt sein muss. shared_ptr kann das, trifft das auf unique_ptr auch zu?

    Mit "nicht bekannt" meinst Du "unvollständig" oder? 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. Letzteres kann man aber eigentlich nur erreichen, wenn man eine solche unique_ptr-Klasse nur als Typ eines privaten Klassenelements verwendet (zB für PImpl). Eine gute Implementierung sollte zumindest so etwas wie checked_delete im default_deleter verwenden, so dass im Falle einer Fehlbenutzung von unique_ptr es einen Compile-Zeit-Fehler gibt.

    Nexus schrieb:

    Das ist einer der Punkte, die mich an C++0x am meisten stören: Diese "Uniformität", nun 4 Schreibweisen für das Gleiche zu haben.

    int a = 4;
    int a(4);
    int a = {4};
    int a {4};
    

    Mindestens auf die letzte hätte man meiner Meinung nach ganz verzichten können. Und besonders mühsam wirds, wenn das Gleiche komplett Unterschiedliches bedeutet.

    std::complex<double> c {3.2, 4.5}; // Ruft den Konstruktor mit 2 Argumenten auf
    std::vector<double> v {3.2, 4.5}; // Initialisierungsliste für vector!
    

    Ist das nicht intuitiver so? Wenn das nicht viele als intuitiv empfunden hätten, hätte man sich die initializer_list-Konstruktoren für Container-Klassen auch gespart, denke ich. Man muss sich nur merken, dass, wenn man den anderen Konstruktor aufrufen will, die Klammern nutzen muss:

    std::vector<double> x {4,2}; // Vektor mit Elementen 4,2
    std::vector<double> x (4,2); // Vektor mit Elementen 2,2,2,2
    

    Das sehe ich jetzt aber nicht als Schwäche der Sprache C++, sondern höchstens als Geschmackssache bzgl des Klassendesigns.

    Nexus schrieb:

    Ich hoffe mal, ich bin immer noch auf dem aktuellen Stand. Falls das wirklich stimmt, haben sich die Leute dabei nichts überlegt? Zum Beispiel dass normaler Code mit Konstruktoraufrufe durch {} plötzlich eine ganz andere Bedeutung bekommt, wenn ein Initialisierungslisten-Konstruktor hinzugefügt wird?

    Wieso "plötzlich eine ganz andere Bedeutung"? Diese Art der Initialisierung für nicht-Aggregat-Klassen gab es doch vorher gar nicht. Es zwingt dich ja keiner, nur noch die geschweiften Klammern zu nutzen.

    Und selbst bei Aggregaten können wir jetzt statt

    aggregat_typ x = {...};
    return x;
    

    auch einfach

    return aggregat_typ {...};
    

    schreiben. Und das ist gut soTM. 🙂



  • Tachyon schrieb:

    Was meinst Du mit kopierendem Smartpointer? Ein Smartpointer, der eine tiefe Kopie erzeugt?

    Genau. Bin ich der einzige, der das je braucht? 😉

    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? Denn im Deleter darf man ja nicht direkt ein delete stehen haben, weil das wieder vollständige Typen erfordert (um korrekt zu zerstören)...

    Ich verwende bei meiner Implementierung Funktionszeiger. Für den Anwender ist es dabei viel einfacher, unvollständige Pointee-Typen zu haben (dafür büsst man natürlich ganz wenig Performance ein, was aber durch new und delete eh meistens irrelevant ist).



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

    krümelkacker schrieb:

    Wieso "plötzlich eine ganz andere Bedeutung"? Diese Art der Initialisierung für nicht-Aggregat-Klassen gab es doch vorher gar nicht. Es zwingt dich ja keiner, nur noch die geschweiften Klammern zu nutzen.

    Ich meine, dass bestehender C++0x-Code, der die geschweiften Klammern für einen "normalen" Konstruktor verwendet, plötzlich fehlerhaft wird (oder noch schlimmer, dennoch kompiliert), wenn in die Klasse ein Konstruktor mit std::initializer_list hinzugefügt wird.

    Was macht man eigentlich bei einem Konstruktor, der als erstes Argument eine std::initializer_list und als zweites ein int nimmt (geht das überhaupt?)? Und wenn die Reihenfolge umgekehrt ist?

    krümelkacker schrieb:

    Ist das nicht intuitiver so? Wenn das nicht viele als intuitiv empfunden hätten, hätte man sich die initializer_list-Konstruktoren für Container-Klassen auch gespart, denke ich.

    Ich finde, man hat es etwas übertrieben, indem man alle möglichen Schreibweisen erlaubt, um es möglichst vielen Recht zu machen. Das neue Sprachmittel std::initializer_list gefällt mir sehr, aber die Umsetzung im Zusammenhang mit der Initialisierung finde ich teilweise sehr fragwürdig. Ideal fände ich so eine Syntax (wobei args eine komma-separierte Liste und expr ein einzelner Ausdruck ist):

    T obj = expr;    // 1. Konvertierungs- oder Kopierkonstruktor
    T obj(args);     // 2. alle Konstruktoren
    T obj = {args};  // 3. impliziter Konstruktor mit std::initializer_list (konsistent mit 1.)
    T obj({args});   // 4. expliziter Konstruktor mit std::initializer_list (konsistent mit 2.)
    

    Man hätte Aufzählungen innerhalb {} einfach als Literal für std::initializer_list interpretieren können, ohne dass es manchmal eine Initialisierungsliste ist, manchmal eine Konstruktor-Argumentliste, und manchmal nur ein einzelner skalarer Wert. Warum folgende Schreibweisen überhaupt möglich sind, sehe ich nicht ein.

    T obj = {expr};  // 5. einzelner Ausdruck, ohne std::initializer_list
    T obj {expr};    // 6. ebenfalls
    T obj {args};    // 7. hier wieder mit std::initializer_list
    

    Das Ganze ist sowas von inkonsistent, ich verstehe nicht wie man das "uniform" nennen kann. Nur weil man die geschweiften Klammern nun für alles einsetzen kann, ist das noch lange nicht einheitlich. Zumal die anderen Schreibweisen ja ungestört weiterexistieren und ein Gemisch vorprogrammiert ist. Ich werde jedenfalls versuchen, möglichst nur die oberen Varianten zu benutzen.



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


Anmelden zum Antworten