Schwarz counter



  • Hi,

    Hume hat letztens mal was von Schwarz Countern erzählt, die es ermöglichen, die Reihenfolge der Instanzierung globaler Objekte zu steuern. Kann mir jemand mal ein verständliches Beispiel geben, wie sowas gemacht wird. Google spuckt leider nur Sachen aus, die mich nicht so richtig weiterbringen.
    Oder gibt es vielleicht noch andere Möglichkeiten, die Instanzierung zu steuern?



  • Hallo,

    Kann mir jemand mal ein verständliches Beispiel geben, wie sowas gemacht wird

    Stell dir vor du brauchst ein globales Objekt log einer Klasse Log. Das Objekt soll wenn möglich als erstes erzeugt (so das auch die Konstruktion globler Objekte geloggt werden kann) und als letztes zerstört werden (so das auch die Zerstörung globaler Objekte geloggt werden kann).
    Der C++ Standard definiert die Konstruktionsreihenfolge (und damit Destruktionsreihenfolge) globaler Objekt nur innerhalb einer ÜE (Reihenfolge der Definition), nicht aber die von Objekten in unterschiedlichen ÜEs. Sobald du also ein globales Objekt foo hast, das in einer anderen ÜE lebt als dein log-Objekt und du foos Konstruktion/Destruktion loggen willst, hast du ein Abhängigkeitsproblem zwischen den beiden globalen Objekten, da dir niemand garantiert welches Objekt zuerst konstruiert (und damit zuletzt destruiert) wird. Mal malt foo zuerst und mal nicht. Nicht schön.

    Eine mögliche Lösung dieses Problems ist die Verwendung eines statischen Counters (also ein Counter pro ÜE).
    Hier mal Beispielcode (mit einem Pointer, da die Verwendung eines Objekts das Beispiel unnötig verkomplitziert)

    // Log.h
    class Log
    {
    public:
        Log() {}
    private:
        class NiftyCounter;
        friend class NiftyCounter;
    };
    extern Log* log;    // dieser Pointer hält unsere globale Instanz
    
    static class Log::NiftyCounter
    {
    public:
        NiftyCounter() { 
            if (count_s == 0) {
            	log = new Log;
            	++count_s;	
            }
    	}
    	~NiftyCounter() {
    		if (--count_s == 0) {
    			delete log;
    		}	
    	}
    private:
        static int count_s;
    } counter_g;	// Dieses statische Objekt ist der eigentliche Trick
    
    // log.cpp
    #include "log.h"
    int Log::NiftyCounter::count_s = 0;
    Log* log = 0;
    

    Und Anwender schreiben:

    // foo.h
    class Foo {
    public:
        Foo ();
        ~Foo();
    };
    extern Foo f;
    
    // foo.cpp
    #include "foo.h"
    #include "log.h"
    Foo::Foo()
    {
        log->construction("Foo");
    }
    Foo::~Foo()
    {
        log->destruction("Foo");
    }
    
    Foo f;  // gloables Objekt
    

    Warum ist das jetzt sicher? Der Trick beim Schwarz-Counter (auch nifty counter genannt) ist der, dass du in jeder ÜE in der du das globale Objekt verwenden willst (im Beispiel log) einen statischen Counter anlegst und zwar *bevor* irgendein Code auf die Funktionen von Log zugreifen kann (um auf die Funktionen einer Klasse zugreifen zu können braucht man die Klassendefinition, sprich man muss *vorher* den Header inkludieren und damit holt man sich unweigerlich den Counter in die ÜE und zwar *bevor* irgendwelche Objekte auf die Klasse zugreifen können). Bei der Konstruktion des ersten Counters erstellst du das globale Log-Objekt. Alle weiteren Counter-Konstruktionen zählen nur einen internen Counter hoch, was dir eine "der-Letzte-macht-das-Licht-aus"-Destruktion ermöglicht.
    Das mit dem internen Counter wiederum funktioniert, da es sich hier um einen Integer handelt, der static-initialization unterliegt und deshalb bereits spätestens beim Linken mit 0 initialisiert wurde.

    Im Beispiel: Angenommen der Compiler/Linker/Loader entscheidet sich dafür, die globalen Objekte in foo.cpp zuerst zu initialisieren. Da Foo von Log abhängig ist, muss foo.cpp log.h inkludieren und zwar bevor irgendeine Methode von Foo auf eine von Log zugreifen kann. Damit holt sich foo.cpp aber ein statisches NiftyCounter-Objekt in seine ÜE und zwar vor die Definition von f.
    Der C++ Standard garantiert mir, dass innerhalb einer ÜE globale Objekte entsprechend ihrer Definitionsreihenfolge konstruiert werden. Also hier: counter_g vor f. Durch die Konstruktion von counter_g wird automatisch ein Log-Objekt erzeugt (falls noch nicht vorhanden).
    Ergo ist die Konstruktion/Destruktion von f nun sicher.

    Um mit dieser Technik globale Objekte zu erstellen (nicht Objekte die über Pointer verwaltet werden), muss man einen Umweg über Placement-New einschlagen und Konstruktion von Initialisierung trennen.
    Initialisiert wird durch die Counter-Klasse.
    Beispiel gibt's bei Bedarf 😉

    Achso, die Technik hat natürlich auch Nachteile. Da jede ÜE die das globale Objekt verwendet dadurch mindestens ein statisches Objekt enthält, hat man falls viele ÜEs das gloable Objekt verwenden, ne ganze Menge statischen Initialisierungscode. Dazu schreiben Cline et. al. in "C++ FAQs - 2nd Edition":

    This means that a large percantage of the application needs to be paged into memory during startup, which can significantly degrade startup performance...



  • Danke für die Erklärung. So langsam lichtet sich der Nebel. Das mit dem static Objekt (counter_g) klingt einleuchtend. Hatte im Netz ein ähnliches Minimalbeispiel gefunden (das was bei dir class Log::NiftyCounter ist), aber halt nur Code ohne Erklärung. Dann ist es relativ schwer, sich daraus betreffende Infos zu holen. Jetzt versteh ich aber auch warum da folgendes gemacht wird

    namespace { InitMgr initMgr; } // one per file inclusion
    

    Das dürfte letztendlich aufs gleiche hinauslaufen, wie

    static class Log::NiftyCounter
    {
    // ...
    } counter_g;
    

    Werd auf jedenfall mal ein bisschen damit rumspielen. Ua brauch ich das für genau so eine Log Klasse.
    Wenn es nicht zuviel Aufwand ist, wäre ich für das Beispiel bei globalen nicht-Zeiger Objekten dankbar.



  • groovemaster schrieb:

    Jetzt versteh ich aber auch warum da folgendes gemacht wird

    namespace { InitMgr initMgr; } // one per file inclusion
    

    Das dürfte letztendlich aufs gleiche hinauslaufen, wie

    static class Log::NiftyCounter
    {
    // ...
    } counter_g;
    

    Jup. Ist bis auf die unterschiedlichen linkage-Eigenschaften von initMgr und counter_g das selbe. Beide erfüllen aber auf jeden Fall den selben Zweck.

    Wenn es nicht zuviel Aufwand ist, wäre ich für das Beispiel bei globalen nicht-Zeiger Objekten dankbar.

    Im Netz kursieren Beispiele wie
    das oder
    das.

    Die Beispiele demonstrieren den entscheidenen Trick, nämlich Trennung von Konstruktion (Aufruf eines noop-Ctors) und Initialisierung (Aufruf eines Init-Ctors über placement-new).
    Hier noch am Beispiel Log:

    #include <new>
    class Log
    {
    public:
        enum NoInit {noInit};
        enum Init {doInit};
    
        // Konstruktor der vom Nifty-Counter aufgerufen wird
        Log(Log::Init) 
        {
        	// Initialisiere Log-Objekt vollständig
        }
    
        // Konstruktor der bei der Initialisierung des globalen Objekts 
        // aufgerufen wird.
        Log(Log::NoInit)
        {
        	// Hier bitte keine wirkliche Initialisierung von *this
        }
        ~Log()				// sollte leer sein
        {}
    
        void deinit();		// enthält den Aufräumcode
    private:
        class NiftyCounter;
        friend class NiftyCounter;
    };
    extern Log log;    // Deklaration des globalen Objekts
    
    static class Log::NiftyCounter
    {
    public:
        NiftyCounter() { 
            if (count_s == 0) {
                // placement-new erzeugt Log-Instanz
    			// im Speicher der globalen Instanz
    			// Diesmal mit Initialisierung
                new (&log) Log(Log::doInit);
                ++count_s;    
            }
        }
        ~NiftyCounter() {
            if (--count_s == 0) {
                log.deinit(); // Aufräumen und...
                log.~Log();   // Dtor von Hand aufrufen!
            }    
        }
    private:
        static int count_s;
    } counter_g;
    
    // log.cpp
    #include "log.h"
    int Log::NiftyCounter::count_s = 0;
    Log log(Log::noInit);	// Definition des globalen Objekts
    

    Auf zwei Dinge musst du dabei aber achten:
    1. Dein Noop-Ctor darf kein Member deines Objekts ändern, da er potentiell nach dem Init-Ctor aufgerufen wird.
    2. Der Destruktor darf keinen Aufräumcode enthalten. Stattdessen muss solcher
    in eine extra Methode die dann explizit durch den letzten Counter aufgerufen wird.

    In meinen Augen ist der Code aber unportabel, da das globale Objekt zweimal geboren wird und zweimal stirbt. Und zwar in dieser Reihenfolge.
    Das ist sicher nicht im Sinne des C++ Lebenszyklusmodells.

    Alternativ kann man gleich zu unportablen Tricks greifen. Beim VC kann man über
    die #pragma init_seg-Anweisung zum Beispiel festlegen, dass für bestimmte Objekte kein Initialisierungscode erzeugt werden soll.
    Ein simples:

    #pragma init_seg("NO_INIT")
    

    vor die Definition des globalen Log-Objekts und alles ist in Butter. In diesem Fall wird auch wirklich nur ein Ctor und ein Dtor aufgerufen (nämlich jeweils durch das zuerst erzeugte Counter-Objekt) und man spart sich das gehample mit den zwei Ctoren bzw. Dtor + Aufräumfunktion.



  • Auf solche nicht standardisierten pragma Geschichten möchte ich eigentlich ungern zurückgreifen. Benutze zwar hauptsächlich Visual C++ samt Compiler, versuche aber immer portabel zu bleiben. Bzw sollte zumindest GCC meinen Code problemlos schlucken.
    Ansonsten nochmals danke für die Beispiele, ich denke ich hab jetzt genug Material, um mir daraus was passendes zu basteln.



  • groovemaster schrieb:

    Auf solche nicht standardisierten pragma Geschichten möchte ich eigentlich ungern zurückgreifen.

    Es ist ja nur eine optimierung, genau wie #pragma once

    notfalls ein #ifdef basteln und gut ist.


Anmelden zum Antworten