Singleton static und lazy creation



  • ogni42 schrieb:

    @nurf: Dein Code ist nicht Exception-sicher

    Danke für den Hinweis! 👍



  • nurf schrieb:

    Martin Kalbfuß schrieb:

    Hallo Leuts,

    type &singleton()
    {
    static type instance;
    return instance;
    }

    Handelt es sich hierbei um "lazy creation"?

    Nein handelt es nicht

    Ich behaupte weiterhin das Gegenteil.

    "instance" wird hier erst erzeugt, wenn singleton() das erste mal aufgerufen wird. Ich nenne das lazy creation bzw. eben lazy initialization.

    Wenn du meinst das wäre nicht so, argumentiere es bitte.



  • hustbaer schrieb:

    Ich behaupte weiterhin das Gegenteil.

    "instance" wird hier erst erzeugt, wenn singleton() das erste mal aufgerufen wird. Ich nenne das lazy creation bzw. eben lazy initialization.

    Sehe ich auch so...



  • im alexandrescu wird das ausführlich diskutiert, wenn gewünscht suche ich gleich die seite raus und zitiere das mal, aber hier ist der sinn den er beschreibt

    statische variablen werden am anfang des programms erzeugt, aber erst bei der ersten benutzung initialisiert. das heisst der compiler reserviert schon den speicher und teilt den zu, aber bereit für die nutzung ist es erst wenn es laut quellcode erzuegt wird, sprich eben in der zeile "static Singleton singleton;"

    und da gibts auch probleme beim initialisieren, wenn nämlich mehrere singletons unternander abhängig sind und sich gegenseitig brauchen zum initialisieren. da bei statischen variablen die initialisierungs reihenfolge nicht festgelegt ist...



  • Also wann der Speicher reserviert wird, spielt aus meiner Sicht mal keine Rolle.

    Und Probleme mit der Initialisierungsreihenfolge bekommt man mit function-static Variablen nicht mehr oder weniger als mit der static Pointer + new Methode.



  • type& singleton() 
    { 
      static std::unique_ptr<type> instance(new type());
      return *instance;
    }
    

    Und schon wird der Speicher dynamisch reserviert, wobei ich die statische Allokation der ursprünglichen Variante eigentlich eher als Vorteil denn als Nachteil sehen würde.

    Ich würd das Ding übrigens nicht unbedingt singleton nennen, denn am Ende verwechselt es noch einer mit dem Singleton Pattern. Auch ist es natürlich von den üblichen Nachteilen globaler Variablen betroffen, weshalb ich von sowas grundsätzlich eher abraten würde, ganz egal wie die konkrete Implementierung am Ende auch aussehen mag...



  • Super Danke für die hilfreichen Antworten. Ich habe es eigentlich nur Singleton genannt, weil mir kein besserer Name eingefallen ist. Gibt es denn einen?



  • Lazy Creation scheint normal zu sein.

    Input:

    #include <string>
    #include <cstdio>
    
    void test()
    {
       static std::string foo = "foo";
       foo += "o";
       puts(foo.c_str());
    }
    
    int main()
    {
       for(int i = 0; i < 10; ++i)
          test();
    }
    

    GCC macht daraus:

    0x08048784 <+0>:	push   %ebp
       0x08048785 <+1>:	mov    %esp,%ebp
       0x08048787 <+3>:	push   %esi
       0x08048788 <+4>:	push   %ebx
       0x08048789 <+5>:	sub    $0x20,%esp
       0x0804878c <+8>:	mov    $0x804a050,%eax
       0x08048791 <+13>:	movzbl (%eax),%eax
       0x08048794 <+16>:	test   %al,%al
       0x08048796 <+18>:	jne    0x804880c <_Z4testv+136>
       0x08048798 <+20>:	movl   $0x804a050,(%esp)
       0x0804879f <+27>:	call   0x8048600 <__cxa_guard_acquire@plt>
       0x080487a4 <+32>:	test   %eax,%eax
       0x080487a6 <+34>:	setne  %al
       0x080487a9 <+37>:	test   %al,%al
       0x080487ab <+39>:	je     0x804880c <_Z4testv+136>
       0x080487ad <+41>:	mov    $0x0,%ebx
       0x080487b2 <+46>:	lea    -0x9(%ebp),%eax
       0x080487b5 <+49>:	mov    %eax,(%esp)
       0x080487b8 <+52>:	call   0x80486a0 <_ZNSaIcEC1Ev@plt>
       0x080487bd <+57>:	lea    -0x9(%ebp),%eax
       0x080487c0 <+60>:	mov    %eax,0x8(%esp)
       0x080487c4 <+64>:	movl   $0x8048970,0x4(%esp)
       0x080487cc <+72>:	movl   $0x804a058,(%esp)
       0x080487d3 <+79>:	call   0x8048660 <_ZNSsC1EPKcRKSaIcE@plt>
       0x080487d8 <+84>:	movl   $0x804a050,(%esp)
       0x080487df <+91>:	call   0x8048650 <__cxa_guard_release@plt>
       0x080487e4 <+96>:	mov    $0x8048640,%eax
       0x080487e9 <+101>:	movl   $0x804a040,0x8(%esp)
       0x080487f1 <+109>:	movl   $0x804a058,0x4(%esp)
       0x080487f9 <+117>:	mov    %eax,(%esp)
       0x080487fc <+120>:	call   0x80485f0 <__cxa_atexit@plt>
       0x08048801 <+125>:	lea    -0x9(%ebp),%eax
       0x08048804 <+128>:	mov    %eax,(%esp)
       0x08048807 <+131>:	call   0x8048670 <_ZNSaIcED1Ev@plt>
       0x0804880c <+136>:	movl   $0x8048974,0x4(%esp)
       0x08048814 <+144>:	movl   $0x804a058,(%esp)
       0x0804881b <+151>:	call   0x80485e0 <_ZNSspLEPKc@plt>
       0x08048820 <+156>:	movl   $0x804a058,(%esp)
       0x08048827 <+163>:	call   0x8048620 <_ZNKSs5c_strEv@plt>
       0x0804882c <+168>:	mov    %eax,(%esp)
       0x0804882f <+171>:	call   0x8048680 <puts@plt>
       0x08048834 <+176>:	add    $0x20,%esp
       0x08048837 <+179>:	pop    %ebx
       0x08048838 <+180>:	pop    %esi
       0x08048839 <+181>:	pop    %ebp
       0x0804883a <+182>:	ret    
       0x0804883b <+183>:	mov    %eax,%esi
       0x0804883d <+185>:	lea    -0x9(%ebp),%eax
       0x08048840 <+188>:	mov    %eax,(%esp)
       0x08048843 <+191>:	call   0x8048670 <_ZNSaIcED1Ev@plt>
       0x08048848 <+196>:	test   %bl,%bl
       0x0804884a <+198>:	jne    0x8048858 <_Z4testv+212>
       0x0804884c <+200>:	movl   $0x804a050,(%esp)
       0x08048853 <+207>:	call   0x8048690 <__cxa_guard_abort@plt>
       0x08048858 <+212>:	mov    %esi,%eax
       0x0804885a <+214>:	mov    %eax,(%esp)
       0x0804885d <+217>:	call   0x80486c0 <_Unwind_Resume@plt>
    

    Was frei von IDA Pro + Hex Rays nach C konvertiert so aussieht:

    int __cdecl test()
    {
      const char *v0; // eax@4
      char v2; // [sp+1Fh] [bp-9h]@3
    
      if ( !(_BYTE)_guard_variable_for_test_void_::foo && __cxa_guard_acquire(&_guard_variable_for_test_void_::foo) != 0 )
      {
        std::allocator<char>::allocator(&v2);
        std::string::string(&test_void_::foo, &unk_8048970, &v2);
        __cxa_guard_release(&_guard_variable_for_test_void_::foo);
        __cxa_atexit(std::string::_string, &test_void_::foo, &_dso_handle);
        std::allocator<char>::_allocator(&v2);
      }
      std::string::operator__(&test_void_::foo, &unk_8048974);
      v0 = (const char *)std::string::c_str(&test_void_::foo);
      return puts(v0);
    }
    


  • @Ethon
    Ich verstehe nicht ganz was du meinst bzw. uns sagen willst.

    Lazy-Creation im Sinne von "Konstruktor wird erst beim ersten Funktionsaufruf ausgeführt" ist bei function-statics nicht nur normal, sondern vom Standard vorgeschrieben.

    Was die Speicher-Allokation für das Objekt selbst angeht, so ist diese in dem von dir gezeigten Code statisch: der "this" Pointer des std::string ist im nach (fast-) C zurückübersetzten Code &test_void_::foo , also statisch. Das Speicher-Besorgen selbst ist als nicht "lazy".



  • dot schrieb:

    type& singleton() 
    { 
      static std::unique_ptr<type> instance(new type());
      return *instance;
    }
    

    [...]
    Ich würd das Ding übrigens nicht unbedingt singleton nennen, denn am Ende verwechselt es noch einer mit dem Singleton Pattern.

    Das ist das Singleton-Pattern.
    Bzw. so knapp dran, dass ich keinen nennenswerten Unterschied sehe.
    Das einzige was hier nicht dem Singleton-Pattern entspricht (bzw. vielleicht - wir sehen zu wenig Code um das sicher sagen zu können) ist, dass die Klasse sich nicht selbst "schützt" und zum Singleton macht.

    Was Vor-/Nachteile angeht ist das IMO aber nicht wirklich wichtig.



  • hustbaer schrieb:

    dot schrieb:

    type& singleton() 
    { 
      static std::unique_ptr<type> instance(new type());
      return *instance;
    }
    

    [...]
    Ich würd das Ding übrigens nicht unbedingt singleton nennen, denn am Ende verwechselt es noch einer mit dem Singleton Pattern.

    Das ist das Singleton-Pattern.
    Bzw. so knapp dran, dass ich keinen nennenswerten Unterschied sehe.
    Das einzige was hier nicht dem Singleton-Pattern entspricht (bzw. vielleicht - wir sehen zu wenig Code um das sicher sagen zu können) ist, dass die Klasse sich nicht selbst "schützt" und zum Singleton macht.

    Die Tatsache dass der Typ gegen Mehfachinstanzierung geschützt wird, ist doch die eine Eigenschaft die ein Singleton ausmacht!? Alles andere (globaler Zugriff auf die Instanz) ist nur Konsequenz einer konkreten Inkarnation des Pattern, nicht aber der Zweck des Singleton Pattern. Das da oben ist in etwa so orthogonal zum Singleton Pattern wie eine globale Variable...

    EDIT: Natürlich gehe ich davon aus dass der Typ nicht geschützt ist. Du hast recht dass man das, basierend auf dem Codeschnipsel, nicht gesichert sagen kann, es ist aber imo ziemlich sicher anzunehmen 😉



  • hustbaer schrieb:

    Was die Speicher-Allokation für das Objekt selbst angeht, so ist diese in dem von dir gezeigten Code statisch: der "this" Pointer des std::string ist im nach (fast-) C zurückübersetzten Code &test_void_::foo , also statisch. Das Speicher-Besorgen selbst ist als nicht "lazy".

    Natürlich landet das in .data/.rdata, anders wäre das ja auch unmöglich umzusetzen.



  • @Ethon/Hustbaer:
    Im von Ethon geposteten assembler listing sind ja schon Mutexe für die Konstruktion drin.

    Macht das speziell nur der gcc oder gilt das z.B. auch bei VC++ (im multithreaded Modell)?



  • Naja, nur GCC will ich jetzt nicht sagen, gibt ja viele Compiler.
    MSVC bis inklusive 2010 macht es allerdings nicht.



  • dot schrieb:

    Die Tatsache dass der Typ gegen Mehfachinstanzierung geschützt wird, ist doch die eine Eigenschaft die ein Singleton ausmacht!?

    Sehe ich jetzt nicht so.
    Wenn ich einen Typ T habe, den man ganz normal instanzieren kann. Und aber eine T Instanz mit spezieller Bedeutung haben kann, die es nur 1x geben kann... es aber trotzdem Sinn macht andere Ts normal zu erzeugen...
    Dann werde ich den Konstruktor von T nicht private machen, und eine "get the ony special T instance" Funktion machen.
    Was für praktisch relevante Unterschiede gibt es da dann zu einem Singleton?
    Für mich ist das einfach das selbe.

    Der Knackpunkt beim Singleton Pattern ist für mich einfach, dass man ein Objekt (nicht eine Klasse) zum Singleton erklärt. Mit all den Vor- und Nachteilen die sich daraus ergeben.

    Für mich besteht auch kein wesentlicher Unterschied zwischen z.B. privaten static Members und Singletons.



  • Was ist überhaupt der Sinn des Singlwton-Patterns? Wieso keine freien Funktionen?



  • hustbaer schrieb:

    Naja, nur GCC will ich jetzt nicht sagen, gibt ja viele Compiler.
    MSVC bis inklusive 2010 macht es allerdings nicht.

    Was bedeutet das denn für den Konstruktoraufruf in dem Fall? Ist der dann nicht mehr thread-safe?



  • Ethon schrieb:

    Was ist überhaupt der Sinn des Singlwton-Patterns? Wieso keine freien Funktionen?

    Die Vorteile des Singleton-Pattern:

    - Man kann sich wie ein OOPer fühlen, während man globale Variablen und Funktionen mit Seiteneffekten benutzt.
    - Es lässt sich praktisch in jedem Programm anwenden.
    - Es muss gut sein, steht schließlich "Pattern" dran.



  • ogni42 schrieb:

    hustbaer schrieb:

    Naja, nur GCC will ich jetzt nicht sagen, gibt ja viele Compiler.
    MSVC bis inklusive 2010 macht es allerdings nicht.

    Was bedeutet das denn für den Konstruktoraufruf in dem Fall? Ist der dann nicht mehr thread-safe?

    Genau, die Initialisierung von function-statics ist mit MSVC nicht threadsafe.

    D.h. wenn man nen Fall hat, wo man nicht garantieren kann, dass die Funktion vollständig ausgeführt wurde, bevor man Threads startet die die selbe Funktion aufrufen, dann kann das in UB enden.

    Ich hoffe dass MS da beim 2012er Studio mal nachbessert.


  • Mod

    Was nebenbei bedeutet, dass nurfs Version auch nicht threadsafe ist (selbst wenn man davon ausgeht, dass die richtige release-aquire-Semantik für ptr gilt und Instruktion nicht zu sehr umgeordnet werden). Das Mutex-Objekt wird dort nämlich auch funktionslokal erzeugt. Und dessen Konstruktion dürfte eher nicht trivial sein. Für solche Zwecke muss ganz klar ein globaler Mutex her, der garantiert bereits konstruiert wurde oder man behilft sich mit einem Eigenbau, der auch mit Zeroinitialisierung noch richtig funktioniert.


Anmelden zum Antworten