Initialisierungs-/Deinitialisierungsreihenfolge



  • Hi, ich würde gerne wissen, ob es garantiert ist, dass globale Objekte in umgekehrter Reihenfolge wie sie erzeugt wurden auch wieder zerstört werden?

    Zum Problem. Ich habe mehrere "globale" Objekte bzw. eigentlich ist das nur ein Trick mittels eines boolschen Flags das mit einer statischen Funktion aufgerufen wird, um das eigentliche statische Objekt in dieser Funktion zu erzeugen und zu registrieren.

    static bool s_bIsRegistered = TypeManager::Register<ClassA>(...); // ClassA.cpp
    static bool s_bIsRegistered = TypeManager::Register<ClassB>(...); // ClassB.cpp
    

    Dazu gibt es noch ein "statische" Klasse "TypeManager" wo diese registriert werden. Damit dies funktioniert (Static Order Fiasco) ist sichergestellt, dass vor dem ersten Registrieren bzw. Erzeugen eines Objektes das registriert werden soll, alle "Member" dieser statischen Klasse "TypeManager" Klasse die sich in statischen Gettern gekapselt befinden initialisiert werden.

    void TypeManager::Destroy()
    {
        // Some cleanup routines...
    }
    void TypeManager::Init()
    {
        static bool s_callOnce = false;
        if (!s_callOnce)
        {
            s_callOnce = true;
            // Make sure all static members are created existent.
            TypeList();
            ....
            Dtor(); // Must be created last so that it gets destroyed first!
        }
    }
    template<class T>
    void TypeManager::Register(...)
    {
        Init();
        ....
    }
    Destructor& TypeManager::Dtor()
    {
        static Destructor s_dtor;
        return s_dtor;
    }
    std::list<Type*>& TypeManager::TypeList()
    {
        static std::list<Type*> s_typeList;
        return s_typeList;
    }
    

    Soweit so gut. Nun gab es neben der "Init" Funktion auch noch eine "Destroy" Funktion die diverse Aufräumarbeiten macht. Die "Destroy" Funktion wurde dabei im Destructor des "Dtor" Objektes aufgerufen. Daher wurde dieses Objekt als letztes Objekt in der "Init" Funktion aufgerufen/erzeugt, damit es dann auch als erstes Objekt dieser Klasse wieder zerstört wird und sicherstellt, dass beim Aufräumen noch alle anderen Member des "TypeManagers" existieren.

    Das ging lange wunderbar, aber seit einiger Zeit habe ich das Problem, dass bereits "TypeManager" interne Objekte zerstört werden noch bevor die globalen registrierten Typen Objekte zerstört wurden. Mir ist ein Rätsel wie es dazu kommen kann. Im Debugger sehe ich auch, dass die TypeManager Klasse während der Initialisierungsphase als aller erstes initialisiert wird (Init Funktion) bevor ein Type Objekt erzeugt/registriert wird. Offensichtlich ist die Reihenfolge der Initialisierungs- bzw. Desinitalisierungsphase nicht (mehr) die selbe. Evtl. kam dieses geänderte Verhalten durch ein Clang Update bzw. gibt es Compiler-Flags die dieses Verhalten ändern können? Jemand eine Idee wie ich diesem Problem auf die Spur kommen kann?

    PS: Der Code ist lediglich Pseudocode in Wirklichkeit gibt es gar keine TypeManager Klasse. Den eigentlichen Code kann ich jedoch nicht veröffentlichen.



  • Redest Du über globale Variablen (in main als normale Variable definiert) oder über statische Variablen?



  • Abgesehen von diesen globalen statischen boolschen "Flags" die ein Trick zur Registrierung sind sind ausnahmslos alle statischen Variablen in statischen Getter-Funktionen gekapselt. Z.B.

    Also immer Konstrukte wie...

    double& MyClass::MyStaticMember()
    {
        static double s_value;
        return s_value;
    }
    


  • @Enumerator sagte in Initialisierungs-/Deinitialisierungsreihenfolge:

    PS: Der Code ist lediglich Pseudocode in Wirklichkeit gibt es gar keine TypeManager Klasse. Den eigentlichen Code kann ich jedoch nicht veröffentlichen.

    Dass das jetzt nicht gerade optimal ist ist dir hoffentlich klar.

    Davon abgesehen... kannst du es vielleicht einfach so machen?

    std::shared_ptr<TypeManagerImpl> TypeManager::GetImpl(...)
    {
        static std::shared_ptr<TypeManagerImpl> const s_impl = std::make_shared<TypeManagerImpl>(...);
        return s_impl;
    }
    
    template<class T>
    bool TypeManager::Register(...)
    {
        auto impl = GetImpl(...);
        // TypeEntry keeps TypeManagerImpl object alive by storing a copy of
        // the std::shared_ptr<TypeManagerImpl>
        static TypeEntry s_typeEntry{..., impl};
        return true;
    }
    


  • Mir erscheint das Vorgehen generell etwas umständlich. Wenn man da auf Meyer Singletons setzt, ist das Thema statische Initialisierung gelöst. D.h. es gibt kein Problem mehr mit der Reihenfolge, da der erste Aufruf woher auch immer das Singleton initialisiert. Sobald man über TypeManager::Instance()->Register(...) etwas von außen registrieren will, wird der Konstruktor von TypeManager getriggert und alle benötigten Resourcen werden so wie das im Konstruktor definiert ist, konstruiert. Dadurch kann man sich die ganzen Getter sparen und braucht auch keine weitere statischen Variablen anzulegen. Wenn dann die einzige Instanz von TypeManager zerstört wird, werden dann auch in der umgekehrten Reihenfolge die nicht statischen Member zerstört.

    Das Problem was verbleibt ist folgendes, die Reihenfolge wie das TypeManager Singleton und die Instanzen der registrierten Klassen zerstört werden, ist nicht definiert. Das Problem kann man durch eine geeignete Wahl von SmartPointer lösen.

    class TypeManager {
        int a, b, c, d;
        TypeManager() : a(1), b(2), c(3), d(4) {}
        ~TypeManager() {}
    public:
        std::shared_ptr<TypeManager> instance () {
            static std::shared_ptr<TypeManager> inst;
            return inst;
        }
    };
    
    static std::shared_ptr<TypeManager> TMI = TypeManager::instance();
    
    int main () {
    }
    


  • @john-0 sagte in Initialisierungs-/Deinitialisierungsreihenfolge:

    Mir erscheint das Vorgehen generell etwas umständlich. Wenn man da auf Meyer Singletons setzt, ist das Thema statische Initialisierung gelöst. D.h. es gibt kein Problem mehr mit der Reihenfolge, da der erste Aufruf woher auch immer das Singleton initialisiert. Sobald man über TypeManager::Instance()->Register(...) etwas von außen registrieren will, wird der Konstruktor von TypeManager getriggert und alle benötigten Resourcen werden so wie das im Konstruktor definiert ist, konstruiert. Dadurch kann man sich die ganzen Getter sparen und braucht auch keine weitere statischen Variablen anzulegen. Wenn dann die einzige Instanz von TypeManager zerstört wird, werden dann auch in der umgekehrten Reihenfolge die nicht statischen Member zerstört.

    Das Problem was verbleibt ist folgendes, die Reihenfolge wie das TypeManager Singleton und die Instanzen der registrierten Klassen zerstört werden, ist nicht definiert. Das Problem kann man durch eine geeignete Wahl von SmartPointer lösen.

    class TypeManager {
        int a, b, c, d;
        TypeManager() : a(1), b(2), c(3), d(4) {}
        ~TypeManager() {}
    public:
        std::shared_ptr<TypeManager> instance () {
            static std::shared_ptr<TypeManager> inst;
            return inst;
        }
    };
    
    static std::shared_ptr<TypeManager> TMI = TypeManager::instance();
    
    int main () {
    }
    

    Hehe, da ist nichts kompliziert. Und ich gehöre auch zu den Programmierern die konsequent alle statischen Objekte in statischen Funktionen kapseln genau um z.B. Static Order Probleme zu vermeiden. Statische oder auch globale Variablen gibt es bei mir im Code grundsätzlich nicht, außer den besagten statischen globalen bool Flags die aber wie gesagt nur ein Hack für die automatische Registrierung der Typen sind. Dafür gibt es leider in C++ keine bessere Lösung. Ein Trauerspiel... Das ist hier aber auch nicht das Problem gewesen. Nach langem Debuggen kam ich auf die Idee, dass die Reihenfolge in der die Objekte zerstört werden womöglich von der Reihenfolge wie zu Libs gelinkt wurden abhängt. Ich linke diese nun in umgekehrter Reihenfolge was das Problem gelöst hat. Dieses Verhalten muss man aber ganz klar als Comiler-Bug bzw. C++ Design-Fehler bezeichnen. In meinem Fall wurde also der TypeManager obwohl dieser als erstes initialisiert wurde nicht wieder als letztes deinitialisiert was das erwartete Verhalten gewesen wäre. Stattdessen wurden später erzeugte Objekte (aus dem Kontext einer anderen Lib) nach dem TypeManager deinitialisiert. Das heißt Anwender müssen die Reihenfolge in der die Libs gelinkt werden berücksichten. Das ist mehr als nur unschön.



  • @Enumerator sagte in Initialisierungs-/Deinitialisierungsreihenfolge:

    Nach langem Debuggen kam ich auf die Idee, dass die Reihenfolge in der die Objekte zerstört werden womöglich von der Reihenfolge wie zu Libs gelinkt wurden abhängt. Ich linke diese nun in umgekehrter Reihenfolge was das Problem gelöst hat.

    Was sind denn das für "Libs"? Das klingt nach dynamischen Bibliotheken (DLL/Shared Object), die eigenen Initialisierungs- und Deinitialisierungscode mitbringen, der dann nicht in der richtigen Reihenfolge ausgeführt wird - ist das korrekt?

    Ich würde sagen, eine DLL, die eigenständig ein Objekt aus einer anderen DLL verwendet, sollte selbst ebenfalls gegen diese andere DLL gelinkt werden. Der Loader des OS sollte die Bibliotheken dann automatisch in der richtigen Reihenfolge entladen.

    Wird stattdessen das Objekt vom Hauptprogramm übergeben, ohne dass die DLL Kenntnis darüber hat, woher es stammt und ohne dass sie dieses in Besitz nimmt, sollte es zumindest irgendeine Deinitialisierungs-Funktion geben, die man am Ende des Hauptprogramms aufrufen kann und die garantiert, dass danach nicht mehr auf das fremde Objekt zugegriffen wird.

    Wenn es davon abhängt, in welcher Reihenfolge die Bibliotheken gelinkt werden, damit das Programm nicht vor die Wand läuft, dann klingt das für mich erstmal nach verbesserungswürdigem Software-Design - auch wenn es schon stimmt, dass einem C++ das ein bisschen einfacher machen könnte (liegt wohl auch an nicht einheitlicher, standardisierter ABI für C++).



  • @Enumerator sagte in Initialisierungs-/Deinitialisierungsreihenfolge:

    Hehe, da ist nichts kompliziert. Und ich gehöre auch zu den Programmierern die konsequent alle statischen Objekte in statischen Funktionen kapseln genau um z.B. Static Order Probleme zu vermeiden. Statische oder auch globale Variablen gibt es bei mir im Code grundsätzlich nicht, außer den besagten statischen globalen bool Flags die aber wie gesagt nur ein Hack für die automatische Registrierung der Typen sind. Dafür gibt es leider in C++ keine bessere Lösung.

    Wenn man ein Meyers Singleton nutzt, eliminiert man bei der Konstruktion die Abhängigkeit von der Linkreihenfolge, was bei Deiner Lösung mit den Flags nicht der Fall ist. Es verbleibt das Problem der Destruktion. Solange alle statischen Objekte, die Du benötigst in einem Singleton gespeichert sind, ist das auch kein Problem. Problematisch sind dann nur dazu gelinkte Bibliotheken, falls diese irgendwelche Zeiger oder Objekte auf den selbst geschriebenen Code halten.

    Ein Trauerspiel... Das ist hier aber auch nicht das Problem gewesen. Nach langem Debuggen kam ich auf die Idee, dass die Reihenfolge in der die Objekte zerstört werden womöglich von der Reihenfolge wie zu Libs gelinkt wurden abhängt. Ich linke diese nun in umgekehrter Reihenfolge was das Problem gelöst hat. Dieses Verhalten muss man aber ganz klar als Comiler-Bug bzw. C++ Design-Fehler bezeichnen.

    Es ist definitiv kein Compilerfehler, er verhält sich korrekt im Sinne der Norm. Es ist ein Designfehler der Sprache, weil man das als implementation defined erlaubte. Ich halte das nicht für gut – aber man hat sich seit vielen Jahren nicht dazu durchringen können, das zu ändern.

    In meinem Fall wurde also der TypeManager obwohl dieser als erstes initialisiert wurde nicht wieder als letztes deinitialisiert was das erwartete Verhalten gewesen wäre. Stattdessen wurden später erzeugte Objekte (aus dem Kontext einer anderen Lib) nach dem TypeManager deinitialisiert. Das heißt Anwender müssen die Reihenfolge in der die Libs gelinkt werden berücksichten. Das ist mehr als nur unschön.

    Anscheinend war Dir das nicht bewusst. Das ist eine der Gründe weshalb man in C++ auf statische Variablen verzichten sollte. Ich habe selbst schon Singletons genutzt, aber noch nie in der Form, dass bei der Destruktion die Reihenfolge ein Rolle gespielt hätte. Wenn ich mich recht entsinne, was das bereits in C++ Komitee Thema und es wurde dafür bisher keine zufriedenstellende Lösung gefunden.


Log in to reply