Gibt es so ein Idiom/Konzept?



  • Hallo,

    ich überlege gerade, in einer Klasse einen alten und neuen Zustand von Variablen zu speichern, um dann nach der "Fertigstellung" des alten Zustandes den neuen Zustand dem alten Zustand zuzuweisen, um dann mit dem neuen Zustand den nächsten Zustand speichern zu können. Nun habe ich mir mal folgenden Code geschrieben, da das Kopieren von größeren Zustandsstrukturen unnötig aufwändig wäre.

    template<typename T> class VariableUpdater
    {
        T *old_elem;
        T *new_elem;
    
        public:
        template<typename... ConstructorArgs> VariableUpdater(ConstructorArgs const &... args)
          : old_elem(NULL)
          , new_elem(NULL)
        {
            //Two-step-initialization for efficient replacement in Update(args...)
            old_elem = static_cast<T *>(malloc(sizeof(*old_elem)));
            new_elem = static_cast<T *>(malloc(sizeof(*new_elem)));
            if(!old_elem || !new_elem) throw std::bad_alloc();
            old_elem = new(old_elem) T(args...);
            new_elem = new(new_elem) T(args...);
        }
    
        ~VariableUpdater()
        {
            old_elem.~T();
            free(old_elem);
            new_elem.~T();
            free(new_elem);
        }
    
        template<typename... ConstructorArgs> void Update(ConstructorArgs const &... args)
        {
            std::swap(old_elem, new_elem);
            new_elem.~T();
            new_elem = new(new_elem) T(args...);
        }
    
        void Update()
        {
            std::swap(old_elem, new_elem);
        }
    
        T &Old()
        {
            return *old_elem;
        }
    
        T &New()
        {
            return *new_elem;
        }
    };
    

    Nun stellt sich mir zuerst die Frage wie im Titel: Gibt es das schon fertig?
    Außerdem suche ich noch nach einem Namen, falls es das noch nicht gibt. StateUpdater?

    mfg,
    wxSkip



  • Ob es das gibt, weiß ich nicht. Ich kann mir gerade keinen Bedarf denken. Aber Deine Implementierung gefällt mir gar nicht.

    wxSkip schrieb:

    old_elem = static_cast<T *>(malloc(sizeof(*old_elem)));
    

    Hast Du Meyers und Alexandrescu nicht gelesen oder nur operator new vergessen oder wähnst Du gar einen tieferen Sinn hinter malloc? Nee, gell.

    wxSkip schrieb:

    if(!old_elem || !new_elem) throw std::bad_alloc();
    

    Und Sutters auch nicht.

    wxSkip schrieb:

    template<typename... ConstructorArgs> void Update(ConstructorArgs const &... args)
        {
            std::swap(old_elem, new_elem);
            new_elem.~T();
            new_elem = new(new_elem) T(args...);
        }
    

    Nee, echt nicht.



  • volkard schrieb:

    Ob es das gibt, weiß ich nicht. Ich kann mir gerade keinen Bedarf denken. Aber Deine Implementierung gefällt mir gar nicht.

    wxSkip schrieb:

    old_elem = static_cast<T *>(malloc(sizeof(*old_elem)));
    

    Hast Du Meyers und Alexandrescu nicht gelesen oder nur operator new vergessen oder wähnst Du gar einen tieferen Sinn hinter malloc? Nee, gell.

    wxSkip schrieb:

    if(!old_elem || !new_elem) throw std::bad_alloc();
    

    Und Sutters auch nicht.

    wxSkip schrieb:

    template<typename... ConstructorArgs> void Update(ConstructorArgs const &... args)
        {
            std::swap(old_elem, new_elem);
            new_elem.~T();
            new_elem = new(new_elem) T(args...);
        }
    

    Nee, echt nicht.

    Ups, das hatte ich schon vermutet, dass das unter den Bereich Premature Optimization fällt...
    Ich wusste nur nicht, ob der Compiler *new_elem = T(args...) in Update() zu einem placement new optimieren kann oder nicht. Aber ich vermute schon.
    Allerdings hast du Recht. Der Meyers steht schon seit Längerem auf meiner Todo-Liste. 🙄



  • wxSkip schrieb:

    ich überlege gerade, in einer Klasse einen alten und neuen Zustand von Variablen zu speichern, um dann nach der "Fertigstellung" des alten Zustandes den neuen Zustand dem alten Zustand zuzuweisen, um dann mit dem neuen Zustand den nächsten Zustand speichern zu können.

    Ähm, willst Du da Transaktionen pr0ggern?



  • volkard schrieb:

    wxSkip schrieb:

    ich überlege gerade, in einer Klasse einen alten und neuen Zustand von Variablen zu speichern, um dann nach der "Fertigstellung" des alten Zustandes den neuen Zustand dem alten Zustand zuzuweisen, um dann mit dem neuen Zustand den nächsten Zustand speichern zu können.

    Ähm, willst Du da Transaktionen pr0ggern?

    Nein. Ich will einen Tokenizer für (fast) beliebige Sprachen schreiben, wozu der Tokenizer eine Language-Klasse übergeben bekommt. Die Language-Klasse bekommt immer wieder einen char übergeben und liefert ggf. als Result die String-Positionen für ein neues Token zurück. Und da man mit einem char eben sowohl das alte Token beenden als auch ein neues anfangen und - im Falle eines Operators - das neue auch gleich wieder beenden kann, braucht man hier eben einen "alten" und einen "neuen" Token-Range. Nachdem die Informationen zum alten Token an den Tokenizer übergeben wurden, wird das "neue" Token zum "alten" Token. Daher also dieser Versuch. Ob man das geschickter machen kann, überlege ich gerade noch.



  • template<typename T> class VariableUpdater
    {
        T *old_elem;
        T *new_elem;
    
        public:
        template<typename... ConstructorArgs> VariableUpdater(ConstructorArgs const &... args)
          : old_elem(NULL)
          , new_elem(NULL)
        {
            //Two-step-initialization for efficient replacement in Update(args...)
            old_elem = static_cast<T *>(malloc(sizeof(*old_elem)));
            new_elem = static_cast<T *>(malloc(sizeof(*new_elem)));
            if(!old_elem || !new_elem) throw std::bad_alloc();  
            old_elem = new(old_elem) T(args...);
            new_elem = new(new_elem) T(args...);
        }
    
        ~VariableUpdater()
        {
            old_elem.~T();
            free(old_elem);
            new_elem.~T();
            free(new_elem);
        }
    
        template<typename... ConstructorArgs> void Update(ConstructorArgs const &... args)
        {
            std::swap(old_elem, new_elem);
            new_elem.~T();
            new_elem = new(new_elem) T(args...);
        }
    
    };
    

    Aaaaaaaaaahhhhhhhhh !!!!! 😮

    Warum tut man so was ? Warum mischt man C-Code mit Klassen ? Ganz zu verschweigen von den undefinierten Verhalten welche du damit herausprovozierst, scheinst du auch einige Speicherlöcher herausprovozieren.

    Tut mir leid aber solcher Code ist Murks.

    Ups, das hatte ich schon vermutet, dass das unter den Bereich Premature Optimization fällt...

    Optimierung sollte in erster Linie immer die Komplexitätsklasse verkleinern und weniger hier und da ein paar Takte einsparen. Der Compiler optimiert auch !

    Ich will einen Tokenizer für (fast) beliebige Sprachen schreiben, wozu der Tokenizer eine Language-Klasse übergeben bekommt.

    Dumme Frage. Wie soll das funktionieren ? Woher wissen deine Programme in welchem (binären) Format deine Language Klasse ist ?

    Schaue dir mal das Broker Pattern an und evt. deren Umsetzung in Windows (Component Objekt Model) an.

    Und ich würde dir erst mal empfehlen eine kleine DLL (oder die entsprechende Unix Variante) zu schreiben.



  • wxSkip schrieb:

    Ich wusste nur nicht, ob der Compiler *new_elem = T(args...) in Update() zu einem placement new optimieren kann oder nicht.

    Kann er nicht. Allerdings solltest zunächst in Ruhe überlegen, ob Destuktor+Construktor tatsächlich effizienter als Zuweisung ist, und warum das so sein könnte. Auf die ganzen Fallen, die das Ganze zum Anti-Pattern machen, muss man schließlich gar nicht eingehen, wenn die Prämisse schon nicht zutrifft.



  • Bitte ein Bit schrieb:

    Aaaaaaaaaahhhhhhhhh !!!!! 😮

    Warum tut man so was ? Warum mischt man C-Code mit Klassen ? Ganz zu verschweigen von den undefinierten Verhalten welche du damit herausprovozierst, scheinst du auch einige Speicherlöcher herausprovozieren.

    Kannst du mir mal bitte erklären, wo hier undefiniertes Verhalten und Speicherlöcher herkommen sollen?

    Bitte ein Bit schrieb:

    Ups, das hatte ich schon vermutet, dass das unter den Bereich Premature Optimization fällt...

    Optimierung sollte in erster Linie immer die Komplexitätsklasse verkleinern und weniger hier und da ein paar Takte einsparen. Der Compiler optimiert auch !

    Auch @camper: Wie gesagt war das für einen Tokenizer gedacht, der zwar nicht von der Komplexitätsklasse, aber dafür vom relativen Zeitverbrauch in der Praxis oftmals anspruchsvoll ist. Wenn ich mir das genauer überlege, steht hier Destruktor + Konstruktor vs. Konstruktor + Kopierkonstruktor (falls vorhanden). Wenn man Glück hat, ist der Kopierkonstruktor inline und kann sogar ganz eliminiert werden. Sollte also keinen großen Unterschied machen.

    Bitte ein Bit schrieb:

    Ich will einen Tokenizer für (fast) beliebige Sprachen schreiben, wozu der Tokenizer eine Language-Klasse übergeben bekommt.

    Dumme Frage. Wie soll das funktionieren ? Woher wissen deine Programme in welchem (binären) Format deine Language Klasse ist ?

    Schaue dir mal das Broker Pattern an und evt. deren Umsetzung in Windows (Component Objekt Model) an.

    Und ich würde dir erst mal empfehlen eine kleine DLL (oder die entsprechende Unix Variante) zu schreiben.

    Ich glaube, du hast das falsch verstanden. Die Language-Klasse wird für jede Sprache (Programmier- oder Codierungs-Sprache) gecoded und bietet ein gleiches Interface nach außen.

    Weil die Language-Klasse momentan fast alles alleine macht, überlege ich mir gerade ein anderes Konzept. Bisher habe ich die Idee, polymorphe Klassen für die Definition von Regeln (nicht so allgemein wie BNF) zu verwenden. Da ich jedoch nicht für jedes einzelne Zeichen zig virtuelle Funktionen aufrufen will, überlege ich, wie ich das statisch (mit Templates o.ä.) umsetzen könnte. Wenn ich dazu noch Fragen habe, melde ich mich noch mal (vermutlich in einem neuen Thread).



  • Kannst du mir mal bitte erklären, wo hier undefiniertes Verhalten und Speicherlöcher herkommen sollen?

    template<typename T> class VariableUpdater
    {
        T *old_elem;
        T *new_elem;
    
        public:
        template<typename... ConstructorArgs> VariableUpdater(ConstructorArgs const &... args)
          : old_elem(NULL)
          , new_elem(NULL)
        {
            //Two-step-initialization for efficient replacement in Update(args...)
            old_elem = static_cast<T *>(malloc(sizeof(*old_elem))); 
            new_elem = static_cast<T *>(malloc(sizeof(*new_elem)));
    // Wer garantiert mir hier das sizeof die richtige Größe der Klasse zurückliefert ? 
            if(!old_elem || !new_elem) throw std::bad_alloc(); 
    // Annahme new_elem = NULL -> Er wirft die Exception und vergisst old_elem freizugeben
            old_elem = new(old_elem) T(args...); 
    // old_elem ist ein Zeiger auf einen reservierten Speicherplatz und er reserviert zusätzlich Speicher für die Klasse.
    // Was ist das überhaupt für ein new() Aufruf ? Und wer gibt old_elem wieder frei bevor dieser mittel new überschrieben wird ?
            new_elem = new(new_elem) T(args...);
        }
    
        ~VariableUpdater()
        {
            old_elem.~T();
            free(old_elem); 
    // Wer garantiert mir das auch die Destruktoren der aggregierten Klassen aufrufen werden ?
    // Bsp.: T = 
    // class Auto { 
    // public: 
    // std::string Marke;   // Wer ruft hier den Destruktor von Marke auf ? 
    // std::string Fahrer;  
    // 
    // //virtual ~Auto();
    //};
    // Kann es sein dass es hier schon Probleme mit virtuellen Destruktoren gibt ?
            new_elem.~T();
            free(new_elem);
        }
    
        template<typename... ConstructorArgs> void Update(ConstructorArgs const &... args)
        {
            std::swap(old_elem, new_elem);
            new_elem.~T();
    // Gleiches Problem wie bei old_elem = new(old_elem) T(args...); 
            new_elem = new(new_elem) T(args...);
        }
    
        void Update()
        {
            std::swap(old_elem, new_elem); // Was tun wenn T kein Kopierkonstruktor enthält ?
        }
    
    // Erlaubt folgendens: delete &VariableUpdater.Old() -> Nicht schön da Klasse in inkonsistenten Zustand gerät.
        T &Old()
        {
            return *old_elem;
        }
    
        T &New() 
        {
            return *new_elem;
        }
    };
    

    Jage deinen Code bitte mal durch Valgrind, Application Verifier oder DebugDiag. Es würde mich mal interresieren was die melden.



  • Bitte ein Bit schrieb:

    old_elem = new(old_elem) T(args...); 
    // old_elem ist ein Zeiger auf einen reservierten Speicherplatz und er reserviert zusätzlich Speicher für die Klasse.
    // Was ist das überhaupt für ein new() Aufruf ? Und wer gibt old_elem wieder frei bevor dieser mittel new überschrieben wird ?
    

    Das ist placement new. Dadurch wird kein neuer Speicher angefordert, sondern nur ein Objekt in dem bereits existierenden Speicher angelegt.



  • @Bitte ein Bit: Du hast bei der malloc-Prüfung recht und (was du nicht explizit gesagt hast) ich habe den Fehler gemacht, xxx_elem.~T() statt xxx_elem->~T() zu schreiben. Ich wüsste jedoch nicht, warum sizeof() etwas falsches zurückliefern sollte und das mit placement new und dem Aufruf der Delete-Funktion solltest du dir nochmal anschauen.
    Bei std::swap hast du vergessen, dass nur die Pointer vertauscht werden und nicht die Objekte, auf die sie zeigen (das ist ja schließlich der Vorteil dieses "Konzepts").
    Und das mit dem delete &updater.Old(); rechtfertigt meiner Meinung nach nicht den komplizierteren Zugriff, der durch Getter-/Setter entstehen würde. Schließlich kannst du genausogut delete &updater; schreiben und es wäre genauso unsinnig.



  • Du hast bei der malloc-Prüfung recht und (was du nicht explizit gesagt hast) ich habe den Fehler gemacht, xxx_elem.~T() statt xxx_elem->~T() zu schreiben.

    Jein, ich meinte du solltest C Befehle nicht mit Klassen aufrufen. Nutze malloc und free für struct's und new/delete für Klassen.

    Aber erstaunlicherweise scheint sizeof() für Klassen unter VS 2008 zu funktionieren. Vermutlich deswegen weil man struct's zu Klassen aufmotzen kann und deswegen eine gewisse Kompatibilität benötigt. Der sizeof Operator scheint bei mir die Anzahl der Bytes der lokalen Memberreferenzen (auf dem Stack) zurückzuliefern + 4 Bytes für vptr. Aber selbst wenn es so aussieht das alles funktioniert muss man sich im klaren sein dass man mit der binären Repräsentation der Klasse agiert und nicht mit der Klasse selbst (was schon gegen dem Grundsatz der OO widerspricht). Und dann kommt schnell man auf den Gedanken eine Klasse mit memcpy kopieren zu wollen und wundert sich dann über das Ergebnis.

    C ist halt C, und C hat, überspitzt formuliert, keine Ahnung von Klassen, Struct's und Strings. Da gibt es nur primitve Datentypen und Aneinanderreihung von diesen.

    Und es bleibt die Frage offen: Wie verhalten sich Konstruktor, Deskruktor Aufrufe bei allen möglichen Klasseneigenschaften (virtueller Deskruktor, ...) ?

    Ich werden mich mit dem Ganzen weiter beschäftigen, da ich schon einmal danach gefragt wurde. Und ich werde bei Gelegenheit das Ganze auch mal ins FAQ stellen.

    Eine Frage noch:
    Warum willst du hier jetzt schon optimieren ? Suche dir einen bitterbösen schnellen Algorithmus für deinen Tokenizer welche die Komplexitätsklasse herunterbricht. Was nützt es dir wenn du hier und du ein paar Takte sparen kannst im Vergleich dazu das dein Alg. um die Hälfte schneller wird ? Lasse dann deinen deinen Compiler mit allen Optierungsstufen los, welcher deinen Code ganz massiv (unter Unmtänden auf deinen Prozessor zugeschnitten) optmieren wird.

    Diese Vorgehensweise ist allemal besser als vorher den Code schon zu optimieren. Denn bei der Fehlerfreiheit schätze ich lesbaren Code was im Gegensatz zu optimierten Code steht, welcher gerne schwer verständlich ist.

    Das ist natürlich aber kein Freibrief für schlechte und redundante Programmierung. 😉

    PS:

    Und das mit dem delete &updater.Old(); rechtfertigt meiner Meinung nach nicht den komplizierteren Zugriff, der durch Getter-/Setter entstehen würde. Schließlich kannst du genausogut delete &updater; schreiben und es wäre genauso unsinnig.

    Dann rufe doch mal nach einem delete &updater.Old(); die Anweisung updater.~VariableUpdater() auf. 🙄



  • Bitte ein Bit schrieb:

    Du hast bei der malloc-Prüfung recht und (was du nicht explizit gesagt hast) ich habe den Fehler gemacht, xxx_elem.~T() statt xxx_elem->~T() zu schreiben.

    Jein, ich meinte du solltest C Befehle nicht mit Klassen aufrufen. Nutze malloc und free für struct's und new/delete für Klassen.

    Ist leider auch falsch.
    Wenn er in C++ schreibt, dann sollte man sowieso auf malloc verzichten. Ein Malloc auf Strukturen(struct)/Klassen(class), welche Konstruktoren besitzen fuehren zu fehlerhaftem Programmverlauf!
    Weil, wie vielleicht schon erwaehnt, der Konstruktor nicht aufgerufen wird.

    Der Coding-Style erinnert mich an wxWidgets, daher kommt wahrscheinlich auch das wxin wxSkip 🤡



  • Aber erstaunlicherweise scheint sizeof() für Klassen unter VS 2008 zu funktionieren. Vermutlich deswegen weil man struct's zu Klassen aufmotzen kann und deswegen eine gewisse Kompatibilität benötigt.

    Es gibt in C++ keinen Unterschied zwischen class und struct , außer dass bei einem struct per default alles public ist und bei einer class alles private .
    Folglich ist es auch nicht erstaunlich, dass sizeof() für Klassen funktioniert.



  • Bitte ein Bit schrieb:

    [Aber erstaunlicherweise scheint sizeof() für Klassen unter VS 2008 zu funktionieren. [...] Der sizeof Operator scheint bei mir die Anzahl der Bytes der lokalen Memberreferenzen (auf dem Stack) zurückzuliefern + 4 Bytes für vptr.

    Wie hast du denn erwartet, dass der sizeof -Operator funktioniert? Selbstverständlich gibt er den Platz zurück, den ein Objekt der Klasse benötigt. Und es gibt auch einige Anwendungsfälle, wo man diesen benötigt.
    Tatsächlich funktioniert auch oftmals das Verschieben von Objekten mit memcpy , das ist laut Standard aber trotzdem UB, weil es abhängig von der Klassenimplementierung auch schief gehen kann (z.B. wenn implizite externe Zeiger auf die Instanz zeigen).



  • Bitte ein Bit schrieb:

    template<typename... ConstructorArgs> VariableUpdater(ConstructorArgs const &... args)
          : old_elem(NULL)
          , new_elem(NULL)
        {
            //Two-step-initialization for efficient replacement in Update(args...)
            old_elem = static_cast<T *>(malloc(sizeof(*old_elem))); 
            new_elem = static_cast<T *>(malloc(sizeof(*new_elem)));
    // Wer garantiert mir hier das sizeof die richtige Größe der Klasse zurückliefert ?       - die Sprachspezifikation, die nebenbei auch garantiert, dass der Speicher hinreichend ausgerichtet ist (außer evtl. in Fällen von  extended alignment i.S.v. C++0x, das können wir mal ignorieren)
            if(!old_elem || !new_elem) throw std::bad_alloc(); 
    // Annahme new_elem = NULL -> Er wirft die Exception und vergisst old_elem freizugeben    - Soweit richtig
            old_elem = new(old_elem) T(args...); 
    // old_elem ist ein Zeiger auf einen reservierten Speicherplatz und er reserviert zusätzlich Speicher für die Klasse.
    // Was ist das überhaupt für ein new() Aufruf ? Und wer gibt old_elem wieder frei bevor dieser mittel new überschrieben wird ?
    //-------
    // Das ist placement new, wenn man auf Nummer sicher gehen will in Template Code würde man schreiben ::new(static_cast<void*>(old_elem)) T(args...); 
    // Aber das ist relativ unwesentlich, wichtiger ist, dass der Construktor von T möglicherweise eine Exception wirft und dann liegt ein Speicherleck vor
    // Auf die Zuweisung des Zeigers könnte man im Übrigen verzichten.
            new_elem = new(new_elem) T(args...);
    // dito
        }
    
        ~VariableUpdater()
        {
            old_elem.~T();
            free(old_elem); 
    // Wer garantiert mir das auch die Destruktoren der aggregierten Klassen aufrufen werden ?   - Die Sprachspezifikation
    // Bsp.: T = 
    // class Auto { 
    // public: 
    // std::string Marke;   // Wer ruft hier den Destruktor von Marke auf ?                      - Der Destruktor von Auto
    // std::string Fahrer;  
    // 
    // //virtual ~Auto();
    //};
    // Kann es sein dass es hier schon Probleme mit virtuellen Destruktoren gibt ?               - nein
            new_elem.~T();
            free(new_elem);
    // Falls nat. eine Updatefunktion vorher mal mit einer Exception ausgestiegen ist, kriegen wird hier ein Problem mit Leichenfledderei, aber das ist
    // kein Fehler des Destruktors selbst
        }
    
        template<typename... ConstructorArgs> void Update(ConstructorArgs const &... args)
        {
            std::swap(old_elem, new_elem);
            new_elem.~T();
    // Gleiches Problem wie bei old_elem = new(old_elem) T(args...); 
            new_elem = new(new_elem) T(args...);
    // Gleiches Problem wie zuvor (Exceptions),
    // außerdem gibt es ein Problem mit Selbstinitialisierung : updater.Update(updater.New());
    // sowohl das swap als auch die Tatsache, dass die Resource zuerst zerstört wird führen zu einem Problem
        }
    
        void Update()
        {
            std::swap(old_elem, new_elem); // Was tun wenn T kein Kopierkonstruktor enthält ?
    // Grundlegender: welchem Zweck dient diese Funktion eigentlich?
        }
    
    // Erlaubt folgendens: delete &VariableUpdater.Old() -> Nicht schön da Klasse in inkonsistenten Zustand gerät.
    // Der Logik kann ich nicht folgen, nach dieser Logik könnte ich auch schreiben
    // vector<foo> x(..);
    // delete [] &x.front();
    // was nat. Unfug ist, aber sicher kein Problem des vector
        T &Old()
        {
            return *old_elem;
        }
    
        T &New() 
        {
            return *new_elem;
        }
    };
    


  • Bitte zwei Bits schrieb:

    Der Coding-Style erinnert mich an wxWidgets, daher kommt wahrscheinlich auch das wxin wxSkip 🤡

    Ich habe durchaus einmal mit wxWidgets gearbeitet, aber mich würde trotzdem interessieren, was du hier konkret mit wxWidgets-Codingstyle verknüpfst.

    @camper: Meinst du mit Leichenfledderei, wenn ein Konstruktor eine Exception wirft und der Destruktor dann Murks macht? Danke für die Hinweise (auch die an Bitte ein Bit), da ich ja anscheinend durch diesen Code an Glaubwürdigkeit verloren habe. 🙄

    Ich werde das Zeugs vermutlich sowieso nicht mehr verwenden (wie gesagt). Ich habe gerade noch Templateprobleme mit der neuen Version am Hals.

    P.S. Die Update-Funktion ohne Parameter war für Fälle gedacht, wo der Ausgangszustand der "neuen" Variable egal ist - oder sie danach noch per updater.New() = T(blah) initialisiert wird.


Log in to reply