static const template Member-Objekt init wie und wo?!



  • Nabend,
    ich dachte ja ich hätt's verstanden.. Mit Basisdatentypen auch kein Problem, aber mit Klassen.. Ich bekomme immer entweder Linker- oder Compiler-Fehler, aber verstehe nicht warum.
    Ich versuche im Header eine static const Array-Objekt in einer Klasse zu "erstellen". Der Header wir dann in die eine Quelldatei eingebunden und (gegen eine lib) kompiliert..
    Ich weiß nicht ob mir die Tatsache, dass es sich um ein Template handelt die Sacher erschwert, oder ob es an der Array-Klasse liegt. Hier mal ein Bsp.:

    template<typename T> class C{
    	const static int statInt = 42; // geht, weil direkt von Compiler eingesetzt, richtig?
    	const static ArrayClass<T> statArray; // egal ob Template, oder nicht?
    //	const static bool statInit = false; // wenn Init in Ctor möglich
    
    public:
    	C(){} // kann man hier Init. mit zus. statInit-Flag?
    };
    
    // oder eben hier, aber das bekomme ich nicht zustande..
    

    Kann mir jemand sagen, welche Möglichkeiten es gibt, und wie die aussehen?!

    So, mal wieder was zu c++ allgemein, und nicht zu irgendwelchen Frameworks die eh fast keiner nutzt ;)..

    MfG
    0xMöbius



  • Hi!

    Nur statische, konstante Integer-Member lassen sich auf diese Weise initialisieren.
    Für andere Typen muss man das außerhalb der Klasse erledigen (ich verwende hier mal std::vector für kompilierbaren Code):

    template<typename T> struct C{
        const static std::vector<T> statArray;
    };
    
    template <typename T>
    const std::vector<T> C<T>::statArray = { 1, 2, 3 };
    

    Frag mich nicht warum das so ist, schliesslich könnte der Compiler eine In-Class-Initialisierung so interpretieren,
    als hätte ich sie wie oben außerhalb der Klasse geschrieben, aber so wird's nunmal derzeit in C++ gemacht.

    Persönlich bevorzuge ich in solchen Fällen oft diese Form der statischen Initalisierung
    (ist nicht immer so notwendig, dennoch halte ich es für ein recht elegantes "Pattern"):

    #include <vector>
    
    template<typename T> struct C{
    
        static const std::vector<T>& getStatArray()
        {
            static std::vector<T> statArray = []()
            {
                std::vector<T> a;
                for (T i = 0; i < 10; ++i)
                    a.push_back(i * i);
                return a;
            }();
            return statArray;
        }
    
        static const std::vector<T> statArray2;
    };
    
    template <typename T>
    const std::vector<T> C<T>::statArray2 = C<T>::getStatArray();
    

    Das sind eigentlich 2 verschiedene Techniken:

    1. Statt statischer Variable eine statische Funktion die lokale statische Variable zurückgibt (Construct On First Use Idiom).
    Das hat den Vorteil dass die statische Variable genau dann erzeugt wird, wenn sie zum ersten mal benötigt wird.
    So wird z.B. die sichere Initalisierung weiterer statischer Variablen welche die erste als Input verwenden ermöglicht
    (siehe statArray2 und diesbezüglich auch das "static initialization order fiasco")

    2. Komplexere Initialisierung mithilfe einer Lambda-Funktion die ein fertig initialisertes Objekt zurückgibt.
    Lokale statische Variablen werden initialisiert wenn der Kontrollfluss des Programms zum ersten mal auf die Deklaration trifft.
    In einem Multithreaded-Programm war das vor C++11 insofern problematisch, als dass der Standard nicht festgelegt hat,
    wer für die Initialisierung verantwortlich ist, wenn zwei Threads die Funktion zeitgleich aufrufen, was nur selten zu schönen Resultaten führte.
    Mit C++11 ist die statische Initalisierung Thread-safe, d.h. es ist Aufgabe des Compilers sicherzustellen, dass nur ein Thread die Variable initialisiert.
    Aus diesem Grund auch die die Lambda-Funktion: Deren Aufruf ist Teil der Initialisierung, d.h. der gesamte komplexe Initialiserungscode kann nur
    von einem Thread aufgerufen werden - äußerst praktisch 🙂

    Gruss,
    Finnegan



  • Danke Finnegan.
    Ich habs dann kurz nach dem Fragestellen doch hin bekommen, was auch sonst.. Ich hatte mich schon gefragt, was ich falsch gemacht habe. Kurz gesagt: Nicht nur das. Die 'eindeutigen' Fehlermeldungen bei Templates taten ihr übriges. Naja...

    Also mit Multithreading habe ich mich noch nicht beschäftigt. Das was du beschreibst, klingt so, als könnte man sich die Finger wund suchen, wenn man nicht weiß, was das Problem ist. Hoffentlich werde ich mich dann an deine Worte hier erinnern..
    Was mich bei der Funktionsgeschichte stört ist, dass man das Problem nur verschiebt - dann wird wichtig, was im Dtor steht.. Aber sonst ne schöne Lösung.

    Ich habe die Quelldateien eigentlich immer als parallel angesehen. Aber könnte man nicht einfach, um das Problem zu lösen, annehmen, dass auch Quelldateien seriell, sprich in einer bestimmten Reihenfolge vorliegen; man also die zuerst kompilieren/linken sollte, die die nötigen Definitionen enthält?! Das wäre sogar konsistent zum Rest - die Dateien selbst sind ja auch seriell aufgebaut (mehr oder weniger).



  • 0xMöbius schrieb:

    Danke Finnegan.
    Ich habs dann kurz nach dem Fragestellen doch hin bekommen, was auch sonst.. Ich hatte mich schon gefragt, was ich falsch gemacht habe. Kurz gesagt: Nicht nur das. Die 'eindeutigen' Fehlermeldungen bei Templates taten ihr übriges. Naja...

    Jo, die Compiler werfen einem da manchmal schon seltsame Fehlermeldungen an den Kopf. Mit der Zeit gewöhnt man sich an die manchmal recht verschrobene Audrucksweise 😉

    0xMöbius schrieb:

    Also mit Multithreading habe ich mich noch nicht beschäftigt. Das was du beschreibst, klingt so, als könnte man sich die Finger wund suchen, wenn man nicht weiß, was das Problem ist. Hoffentlich werde ich mich dann an deine Worte hier erinnern...

    Das ist keine Lösung speziell für Multithreading. Ich wolle damit nur als weiteren Vorteil hervorheben, dass man auf diese Weise mit C++ die thread-sichere initialisierung "geschenkt" bekommt.
    Abgesehen davon ist das sehr oft nicht relevant, da wohl in dein meisten Programmen die statischen Variablen initialisiert werden, bevor ein weiterer Thread ins Spiel kommt.
    Wenn es jedoch relevant ist, dann ist es meistens so ein Ding bei dem man sich tagelang die Haare raufen kann.

    0xMöbius schrieb:

    Was mich bei der Funktionsgeschichte stört ist, dass man das Problem nur verschiebt - dann wird wichtig, was im Dtor steht.. Aber sonst ne schöne Lösung.

    Ich kann nicht ganz nachvollziehen was du damit meinst. Der Destruktor der Klasse hat damit nicht wirklich zu tun.
    Das ist eine statische Funktion mit einer ebenfalls statischen (!), lokalen Variablen. Die wird einmal initialisiert und existiert so lange das Programm läuft.
    Abgesehen davon, dass diese Variable erst beim ersten Aufruf der Funktion initialisiert wird, ist sie äquivalent zu einer statischen Klassenvariablen
    (hängt also nicht an einer Instanz der Klasse C und muss daher auch im Destruktor nicht berücksichtigt werden).

    Gruss,
    Finnegan



  • Ich kann nicht ganz nachvollziehen was du damit meinst.

    Ich habe die Infos aus dem FAQ.. Wenn man das Objekt aufm Heap erstellte, würde es nicht zur Programmlaufzeit zerstört (sondern hoffentlich vom OS), was problematisch wird, wenn im Dtor noch irgendwas wichtiges gemacht werden soll.
    Wenn es doch zerstört wird, also auf dem Stack erstellt wurde, muss man darauf achten, dass es nicht von anderen weiter genutzt wird (zB. statischer Dtor).
    Die 3. Möglichkeit "Nifty Counter" fehlt leider (zumindest im FAQ).
    Es scheint also kein wirkliches Allheilmittel zu geben ... außer vielleicht anzunehmen, dass die Quelldateien doch eine Reihenfolge besitzen 😉


  • Mod

    Nur statische, konstante Integer-Member lassen sich auf diese Weise initialisieren.

    Das ist so nicht korrekt. Siehe hier. 🤡



  • 0xMöbius schrieb:

    Ich kann nicht ganz nachvollziehen was du damit meinst.

    Ich habe die Infos aus dem FAQ.. Wenn man das Objekt aufm Heap erstellte, würde es nicht zur Programmlaufzeit zerstört (sondern hoffentlich vom OS), was problematisch wird, wenn im Dtor noch irgendwas wichtiges gemacht werden soll.
    Wenn es doch zerstört wird, also auf dem Stack erstellt wurde, muss man darauf achten, dass es nicht von anderen weiter genutzt wird (zB. statischer Dtor).
    Die 3. Möglichkeit "Nifty Counter" fehlt leider (zumindest im FAQ).
    Es scheint also kein wirkliches Allheilmittel zu geben ... außer vielleicht anzunehmen, dass die Quelldateien doch eine Reihenfolge besitzen 😉

    Ahh... jetzt verstehe ich. Ja, wenn man wirklich solche komplizierten Abhängigkeiten zwischen den statischen Variablen hat, dann wirds wohl haarig.
    Habe das Problem bisher allerdings noch nicht selbst gehabt. Auf meine statischen Variablen habe ich bisher nicht nach der main() zugegriffen.
    Objekte bei denen ich damit Probleme bekommen hätte waren stattdessen meist lokale Variablen in der main(), ähnlich wie sowas hier:

    int main()
    {
       Application app;
       app.run();
    }
    

    Da hat man dann solche Probleme nicht.

    Finnegan



  • Arcoth schrieb:

    Nur statische, konstante Integer-Member lassen sich auf diese Weise initialisieren.

    Das ist so nicht korrekt. Siehe hier. 🤡

    Ah. Danke für den Hinweis. Meine private In-Memory-Teilkopie des Standards wurde aktualisiert 😉
    constexpr FTW! (hat schon so manche TMP-Konstrukte vereinfacht, mittlerweile ist zum Glück auch MSVC diesbezüglich nicht mehr ganz so rückständig :D)


Log in to reply