Objektzeiger von TListBox auf Gültigkeit prüfen?



  • Guten Abend in die Runde,

    in einer ListBox möchte ich gern den Namen von user-erzeugten Objekten (einer eigenen Klasse) anzeigen lassen und über ne Funktion auf bestimmte Eigenschaften des verknüpften Klassenobjekts zugreifen. So weit so gut.
    Über

    listbox->Items->AddObject(obj->getName(), reinterpret_cast<TObject*>(obj))
    

    kann ich mir das verknüpfen und andersrum auch wieder drauf zugreifen.

    Was ist jetzt aber wenn das Objekt gar nicht mehr existiert? Da ich die Liste bei Änderungen laufend aktualisieren lassen möchte, würd ich gern sowas schreiben:

    if ( !reinterpret_cast<myclass*>(listbox->Items->Objects[j]))
    {
     listbox->Items->Delete(j);
    }
    

    aber funktioniert natürlich nicht (im ungünstigen Fall sogar doch), weil die Speicheradresse zwar freigegeben ist, aber der Zeiger der ListBox ja davon unberührt bleibt. (und das könnte irgendwann dann böse enden..).

    Gibt es irgendne Möglichkeit zu prüfen, ob das Objekt worauf der ListBox-Eintrag zeigt, noch existiert?
    (In der Hilfe steht ja extra, dass die ListBox nicht der Owner wird und ich das Objekt somit selbst freigeben muss, falls der Eintrag gelöscht wird. Ist ja auch gut so, nur andersrum, wie ich den Eintrag da wegkriege, wenn ich das Objekt irgendwo anders lösche, wird nicht beschrieben.)

    Ich könnt ja im Destruktor der Klasse das Element aus der ListBox löschen lassen, aber dann müsste ich da ja dort nochmal die gesamte Listbox nach diesem Objekt durchsuchen, wenn es denn überhaupt in irgendner Liste steht. Ich würd das gern alles in ner "Refresh"-Funktion zusammenhaben und nur einmal die Liste nach Änderungen untersuchen.
    Ich könnt mir natürlich auch beim Löschen eines Objekts ne Liste erstellen, wo ich mir die Adressen der gelöschten Objekte merke und dann bei Aktualisieren der Liste prüfe, ob der Zeiger ne Adresse hat, die in der "Löschliste" steht.

    Aber das geht doch bestimmt noch irgendwie schöner, oder?



  • Ich habe das mal so gelöst, das ich die erzeugten Objekt Instanzen im Konstruktur in ein Set abgelegt habe und im Destruktor dann wieder daraus entfernt habe. Dann kann man einfach prüfen, ob die Objektinstanz noch existiert:
    Einfaches Beispiel:

    class myclass
    {
    public:
      mylcass(){Instances.insert(this);}
      ~myclass(){Instances.erase(this);}
    
      static bool exists(myclass *pobj)
      { return Instances.count(pobj) >0;}
    
    private:
      static std::unordered_set<myclass*> Instances;
    
    }
    

    Nun kann man, wenn man irgendwo her auf das Objekt zu greifen will, so prüfen, ob dieses noch existiert:

    if (myclass::exists(obj))
    {
      //Objekt existiert noch.
    }
    


  • Das Konservieren eines Zeigers über die Lebenszeit des referenzierten Objekts hinaus führt zu undefined behavior:
    https://kristerw.blogspot.de/2016/04/dangling-pointers-and-undefined-behavior.html
    https://trust-in-soft.com/dangling-pointer-indeterminate/

    Du solltest also in jedem Fall darauf achten, den Zeiger aus der Listbox zu entfernen, bevor das Objekt gelöscht wird.

    Ich würde die Listbox sowieso im virtuellen Modus verwenden (cf. TListBox::Style , TCustomListBox::OnData ), dann kannst du die Daten nach Belieben selbst verwalten, anstatt auf TStringList angewiesen zu sein.



  • ok danke euch beiden schonmal! 🙂
    Die Idee von Burkhi find ich gar nicht so schlecht, wobei es laut den Artikeln ja auch zu undefined behavior führen soll, da über die Count-Methode ja auch nen Vergleich gemacht wird.
    *Den Sinn davon versteh ich allerdings noch nicht ganz, warum der Wert des Pointers unbestimmt wird, wenn das referenzierte Objekt gelöscht wird.

    Once the memory is deallocated, the mapping is no longer guaranteed to exist. Use of the segment descriptor might now cause an exception, or the hardware addressing logic might return meaningless data.

    Ich greif ja nicht drauf zu, also warum darf ich nicht mit dem alten Wert als ID zum vergleichen arbeiten? Auch scheint mir das ja ein bisschen veraltet zu sein:*

    The reason for the standard to make this undefined behavior is that even simple operations on dangling pointers, such as assignment or comparison, may misbehave and result in exceptions or arbitrary values on some architectures.At least on architectures that existed when the first version of the standard was written.

    Finde ich jetzt nicht so wirklich nutzerfreundlich.. Aber dann bleibt mir ja eigtl nichts übrig, als den Eintrag doch im Destruktor rauszunehmen und die ganze ListBox zu durchsuchen.

    Nun noch ne kleine Frage zur virtuellen ListBox: Was ist denn der Vorteil daran? Ja ok, ich hab keine StringList mehr, aber letztlich speicher ich doch auch nur einzelne Strings mit nem index ab und ordne dem ein Objekt zu. Woanders hab ich gelesen, dass das bessere Performance liefert bei großen Listen, aber warum? Oder ist da nochwas, was ich übersehe?



  • Benutzerfreundlichkeit steht m. E. auf der Prioritätenliste der C- und C++-Standards relativ weit unten.

    drummi schrieb:

    Nun noch ne kleine Frage zur virtuellen ListBox: Was ist denn der Vorteil daran? Ja ok, ich hab keine StringList mehr, aber letztlich speicher ich doch auch nur einzelne Strings mit nem index ab und ordne dem ein Objekt zu.

    Weil du nicht mehr an die TStringList gebunden bist, könntest du dann z.B. auch eine zusätzliche Indirektion einführen, durch die sowohl das undefinierte Verhalten als auch der lineare Lookup vermieden werden können:

    class BetterStringList
    {
    public:
        struct Entry
        {
            String text;
            std::int64_t objectId;
        };
    
    private:
        std::vector<Entry> entries;
    
    public:
        ... //
    };
    
    class ObjectManager
    {
    private:
        std::int64_t nextObjId_ = 0;
        std::unordered_map<std::int64_t, TObject*> objects_;
    public:
        std::int64_t insert(TObject* obj)
        {
            objects_[nextObjId_] = obj;
            return nextObjId_++;
        }
        bool contains(std::int64_t id) const
        {
            return objects_.find(id) != objects_.end();
        }
        TObject* tryGet(std::int64_t id) const
        {
            std::unordered_map<std::int64_t, TObject*>::const_iterator it = objects_.find(id);
            return it != objects_.end()
                ? it->second
                : NULL;
        }
        ...
    };
    

    (sollte auch bcc32-kompatibel sein)

    Aber ehrlich gesagt fände ich es besser, wenn du die Objektverweise ganz simpel aus der Stringliste löschst, bevor sie ungültig werden.



  • Alles klar, werde mich mal damit auseinandersetzen. Danke für Eure Hilfe! 🙂



  • drummi schrieb:

    ok danke euch beiden schonmal! 🙂
    Die Idee von Burkhi find ich gar nicht so schlecht, wobei es laut den Artikeln ja auch zu undefined behavior führen soll, da über die Count-Methode ja auch nen Vergleich gemacht wird.....

    Es wird die übergebene Adresse geprüft, ob diese im Set vorhanden ist. Da der Destruktor diese ja automatisch aus dem Set entfernt, stellt dies kein Problem dar, wenn dann mit einer nicht mehr existierende Adresse geprüft wird, ob die Instanz noch existiert.

    Allerdings stimme ich Audacia zu, das es sauberer ist, das solche Verweise besser vorher aus solch einer Liste entfernt werden sollten, bevor die Instanz freigegeben wird. Letztendlich kann man ja auch seiner Klasse im Konstruktor einen Zeiger auf so eine Liste übergeben, aus der sie sich dann im Destruktor ganz von selbst austrägt.



  • @burkhi sagte in Objektzeiger von TListBox auf Gültigkeit prüfen?:

    Es wird die übergebene Adresse geprüft, ob diese im Set vorhanden ist. Da der Destruktor diese ja automatisch aus dem Set entfernt, stellt dies kein Problem dar, wenn dann mit einer nicht mehr existierende Adresse geprüft wird, ob die Instanz noch existiert.

    In C führt das trotzdem zu UB: "The value of a pointer becomes indeterminate when the object it points to (or just past) reaches the end of its lifetime." Weil der Zeiger p in der Stringliste "indeterminate" ist, darf der Compiler jeden Vergleich p == q, auch für gültige Zeiger q, einfach durch false substituieren.

    Allerdings habe ich an anderer Stelle nicht richtig aufgepaßt; das fragliche Zitat ist nämlich aus dem C-Standard. Der C++-Standard äußert sich dazu geringfügig anders:

    When the end of the duration of a region of storage is reached, the values of all pointers representing the address of any part of that region of storage become invalid pointer values.
    Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior.
    Any other use of an invalid pointer value has implementation-defined behavior. [35]

    [35] Some implementations might define that copying an invalid pointer value causes a system-generated runtime fault.

    Implementation-defined könnte m. E. ebenso bedeuten, daß der jeweilige Compiler den Zeiger für indeterminate erklärt und trotzdem dieselbe Optimierung anbringt. Wie die Fußnote nahelegt, kann man je nach Implementierung auch in handfestere Probleme wie Segfaults geraten. Es kann aber eben auch sein, daß der jeweilige Compiler für die Verwendung ungültiger Zeiger das intuitiv erwartete Verhalten garantiert.

    Doch selbst wenn das Verhalten intuitiv und wohldefiniert wäre, ist von dieser Technik abzuraten, da sie zu logischen Fehlern führen kann. Beispiel:

    1. Der Ausgangszustand sei folgender:
      Objekt A @ 0x0080000A
      Stringliste: { ("A", 0x0080000A) }
      Hashset: { 0x0080000A }
    2. Das Objekt A wird aus dem Hashset entfernt und dann mit delete freigegeben.
      Stringliste: { ("A", 0x0080000A) }
      Hashset: { }
    3. Ein neues Objekt B wird erzeugt. Der Allokator vergibt die soeben freigewordene Heap-Adresse erneut.
      Objekt B @ 0x0080000A
      Stringliste: { ("A", 0x0080000A), ("B", 0x0080000A) }
      Hashset: { 0x0080000A}
    4. Die Stringliste wird aufgeräumt, indem alle Einträge mit nicht im Hashset vorkommendem Zeiger entfernt werden. Allerdings ist 0x0080000A nun wieder ein gültiger Zeiger, so daß der erste Eintrag bleibt und, entgegen seiner Beschriftung, auf Objekt B verweist.


  • Klare Sache, dass mein Beispiel natürlich auch zu logischen Fehlern, wie Audacia als Beispiel erwähnt hat, führen kann.
    Letztendlich überprüft es anhand des eingebundenen Sets ja auch nur, ob an der zu prüfenden Adresse eine entsprechende myclass Instanz existiert, aber nicht, ob es immer noch die gleiche Instanz ist, die ursprünglich mal erzeugt wurde.
    😉 😉



  • Ja das ergibt Sinn. Danke für die Erläuterungen! Hab das jetzt so gelöst, dass alle mit dem Objekt verknüpften Einträge direkt im Destruktor entfernt werden. Dann gibt's erst keine ungültigen Zeiger.

    mal ne kurze andere Frage nebenbei (ohne extra nen neuen Thread aufmachen zu müssen): habt ihr auch das Problem, dass das RAD-Studio beim Verwenden der Programmierhilfe von CodeInsight öfters hängt oder abstürzt? Und wenn ja, hat jmd das neueste Update und weiß, ob das behoben ist (konnt ich aus dem changelog nicht wirklich entnehmen)? Macht mich bald wahnsinnig, hab den Delay schon ganz hoch gestellt.



  • @drummi CodeInsight stürzt bei mit unter Verwendung der CLANG Compiler ständig ab. Mit dem klassischen Compiler bcc32 tut es eigentlich immer. Verwende die Versionen Seattle und Berlin.



  • @drummi
    Mal geht bei mir die Codevervollständigung und mal gibt es eine Exception. Unter Rad Studio Berlin kam es bei mir tatsächlich auch vor, das das ganze Studio "mitgerissen" wurde. Unter Tokyo 10.2.3 ist es besser geworden, zumindest stürzt nicht mehr das ganze Studio dann ab, trotzdem ärgerlich.

    Ich benutze auch den Clang Compiler. In den Option unter "Codeinsight" habe ich folgende Optionen abgewählt:

    • Code- Parameter
    • Symbolinfo durch Kurzhinweis
    • Autom. vervollständigen
    • Hinweise

    Und die Verzögerung steht auch auf "Hoch".



  • na dann weiß ich zumindest, dass es nicht an mir oder einer grundsätzlich falschen Einstellung liegt, danke für eurer Feedback!