Init Funktion, Konstruktor, App-Klasse



  • Hallo zusammen,

    Ich würde mal gerne eure Meinung hören zum Thema Init-Funktionen, Konstruktoren und "Application"-Klassen sowie statischer und dynamischer Speicherallokation.

    Ich hatte in der Projektbesprechnung vorgeschlagen ein zentrales Application-Objekt zu benutzen. Das dann im Launch-Thread (WinMain, Main) dynamisch zu erzeugen.
    Wenn also die Klassenmember der App-Klasse dann wie folgt schreibe

    Aoo aoo_;
    Boo boo_;
    ...
    Zoo zoo_;
    

    dann werden sie in den gleichen Speicherbereich geladen wie die App-Klasse.
    Sollte die Anwendung "zeitkritisch" arbeiten (Messprogramm, Spiel, ...) dann benötige ich keine zusätzliche Allokation, wie z. B. die Member auf dem Heap.

    Ich mag keine Init-Funktionen, dafür sind Konstruktoren da!

    Man hat die Auswahl zwischen Default-Konstruktor oder spezielle Konstruktoren.
    Ein Defaultkonstruktor (der Application Klasse) sollte dann auch das Objekt nur "default" konstruieren, alle Member dann auch default konstruieren. Das bedeutet z. B. "std::unique_ptr" member besitzen nix, Containerklassen-Member besitzen einen leeren Container (default Konstruktor).

    Beim Application-Objekt sollte statische Speicherallokation vermieden werden. Die Lebenszeit des Objekt erstreckt sich vom Start des Prozesse (Runtime) bis zum Ende des Prozesses und macht eine leidige Init-Funktion nicht unbedingt überflüssig. Exceptions in "globalen" Konstruktoren sind auch nicht wirklich schön.

    Zeiger können statisch allokiert (global) werden.

    namespace global{
       App *app;
    }
    

    In UI-Anwendugen sind viele UI-Funktionen der Betriebssysteme Callbacks und somit auch global, dann kann ein globaler Zeiger schon sinnvoll sein.

    Viele meiner Mitstreiter wollen allerdings Init-Funktionen, die Application-Klasse global...und meiner Meinung damit nicht wirklich übersichtlichen Programmcode in C++.

    Gruß



  • one two three schrieb:

    Ich mag keine Init-Funktionen, dafür sind Konstruktoren da!

    👍

    Aber eine App-Klasse, die alle Objekte enthält? Warum? Was ist schlecht am Heap, wenn man RAII benutzt?

    Objekte sollten nach Möglichkeit dann instantiiert werden, wenn alle benötigten Werte vorhanden sind. Defaultkonstruktoren und zugehörige isValid-Funktionen sind auch nicht viel besser als Initfunktionen. Wenn es sich nicht vermeiden lässt, lieber std::optional (oder boost).



  • manni66 schrieb:

    one two three schrieb:

    Ich mag keine Init-Funktionen, dafür sind Konstruktoren da!

    👍

    Aber eine App-Klasse, die alle Objekte enthält? Warum? Was ist schlecht am Heap, wenn man RAII benutzt?

    An RAII ist nix schlecht. Stellt sicher, das die Resource vernünftig erzeugt und zerstört wird.

    Bislang mag ich App-Klassen, vielleicht auch...weil sie in vielen Frameworks vorkommt und weil damit eine zentrale Klasse geschaffen wird, die eben die "Anwendung" verwaltet. Ich weiß auch das die Klasse dann recht umfangreich wird.

    Meine Idee war/ist

    namespace global{
        App *app = {};
        AppImpl *app_impl = {};
    }
    

    Die App-Klasse ist eine abstrakte Klasse für den Zugriff auf DLL oder Libs und die "Betriebssystem"-Klasse dann die AppImpl.

    im Launchthread habe ich dann z. B.

    auto app = std::make_unique<AppImpl>(std::move(traits));
    global::app = app.get();
    global::app_impl = static_cast<AppImpl *>(global::app); // cast is possible!
    

    Damit ist ja die App-Klasse auf dem Heap und auch alle in der App-Klassen "enthaltenden" Klasse, wie eben Aoo, Boo usw.

    Berechtigter Einwand ist sicher gegen eine fette Application-Klasse.

    Jo, ein Default-Konstruktor macht sicher nur das Nötigste, aber ich finde...ein Defaultkonstruktor der App sollte dann auch nur die Member-Klassen default konstruieren.

    Ist es sinvoll die Memberklassen komplett (vollständig) im Konstruktor zu initialiseren sollte auch die Elternklasse (App) einen entsprechenden Konstruktor besitzen. Damit ist dann klar sichtbar, das dass komplette System vollständig initialisiert ist.

    Subsystem die noch nicht vollständig initialisiert werden können, weil erst im Laufe der Anwendung klar ist (z.B. durch den Nutzer) werden default initialisiert.

    Aber bei der App-Klasse bin ich mir auch nicht wirklich sicher, weil die kann schnell saumäßig umfangreich sein und alles mögliche beinhalten



  • manni66 schrieb:

    one two three schrieb:

    Ich mag keine Init-Funktionen, dafür sind Konstruktoren da!

    👍

    Aber eine App-Klasse, die alle Objekte enthält? Warum? Was ist schlecht am Heap, wenn man RAII benutzt?

    Vielleicht befürchtet man unvorhersagbares Zeitverhalten bei der Speicheranforderung. Es gibt keine Garantie, wie schnell das BS einen passenden Block findet. Vielleicht hat man irgendwo mal gehört, dass malloc in Echtzeitanwendungen böse ist.

    Völlig aus der Luft gegriffen ist das ja auch nicht, und wenn man harte Echtzeitanforderungen hat und die Möglichkeit besteht, alle jemals benötigten Resourcen statisch bereitzustellen, dann ist das nicht die schlechteste Idee. Ab einer gewissen Komplexität der Software ist das bloß nicht mehr praktikabel.

    Aber wenn man alles erst default-konstruiert (mit null_ptr und so weiter) und später, wenn die nötigen Daten verfügbar sind, nochmal "nach-initialisiert", dann gibt es genausowenig eine Garantie, dass das echtzeitgerecht passieren wird. Es ist egal, ob man spät konstruiert oder spät initialisiert.

    Und den grundlegenden Unterschied zwischen global App app und global App* pApp verstehe ich auch nicht.

    Konstruktoren (ich meine die richtigen mit den erforderlichen Parametern) haben gegenüber Init-Funktionen den Vorteil, dass sie garantiert auf einem Objekt genau einmal aufgerufen werden. Init-Funktionen können mehrfach ausgeführt oder auch ganz vergessen werden.



  • one two three schrieb:

    Berechtigter Einwand ist sicher gegen eine fette Application-Klasse.

    Jo, ein Default-Konstruktor macht sicher nur das Nötigste, aber ich finde...ein Defaultkonstruktor der App sollte dann auch nur die Member-Klassen default konstruieren.

    Sinn eines Konstruktors ist, ein Objekt in einen definierten Anfangszustand zu bringen, der keine Zufälligkeiten enthält. Insofern ist ein Default-Konstruktor, der alles auf Null setzt, besser als nichts.

    Andererseits ist ein parametrisierter Konstruktor aber erheblich besser als ein Default-Konstruktor, der ein halbfertiges Ding hinterlässt, an dem nachträglich noch jede Menge Hand angelegt werden muss.

    Die "große App-Klasse" sollte daher meiner Meinung nach keinen Default-Konstruktor haben, sondern der Programmierer sollte gezwungen werden, soviel wie möglich betriebsfertig hineinzukonstruieren.

    Kann man die Teile, die später dazukommen, vielleicht extern konstruieren und über Add-Methoden hineingeben?



  • Printe schrieb:

    Und den grundlegenden Unterschied zwischen global App app und global App* pApp verstehe ich auch nicht.

    Nun ja, ich hatte im statischen Speicher nur Zeiger haben wollen. Denn alles was im statischen steht wird ja ins Image "gebrannt". Und da Speicher aus der Sicht des Speichers eben Speicher ist, sollte der Heapspeicherzugriff auch nicht wesentlich langsamer sein als der Statische. Der statische Speicher und der Stack-Speicher sind, wenn ich recht informiert bin, größenbeschränkt. Der Heap nicht.

    Die Zeiger können dann, recht easy, in globalen UI-Funktion des Betriebssystems genutzt werden.

    Und das die App-Klasse jemals so groß wird (mit allem Drum und Dran, Buffer,...), das sie als Speicherblock nicht mehr allokiert werden kann, denke ich auch nicht.

    Un ja, das Programm sollte soviel Speicher allokieren wie möglich (benötigt), damit spätere oder immer wiederkehren Operationen nicht permanent...Speicher allokieren...Speicher deallkokieren müssen.

    Die erste Idee in der Besprechnung war eben...eine App-Klasse zu bauen und diese dann in den statischen Speicher zu hauen, alle Member dann eben auch in den statischen...damit die Anwendung schneller läuft.

    Und so kam die Idee mit Init-Funktionen auf...die ich, wie schon erwähnt, für die blödeste aller Zeiten halte.



  • one two three schrieb:

    Nun ja, ich hatte im statischen Speicher nur Zeiger haben wollen. Denn alles was im statischen steht wird ja ins Image "gebrannt". Und da Speicher aus der Sicht des Speichers eben Speicher ist, sollte der Heapspeicherzugriff auch nicht wesentlich langsamer sein als der Statische. Der statische Speicher und der Stack-Speicher sind, wenn ich recht informiert bin, größenbeschränkt. Der Heap nicht.

    Ähm ... wie meinen? Ins Image gebrannt? Größenbeschränkt?



  • Ich dachte immer nur der Heap kann beliebig wachsen, solange bis das Betriebssystem abkackt und keinen mehr anfordern kann (hängt sicher auch von der Größe des Blocks ab).

    Static- und Stackspeicher kann nicht unbegrenzt wachsen.

    Der "Static" ist doch in der Exe (Image). Es wird doch beim Start des Prozesses automatisch allokiert...



  • Irgendwie liest sich das für mich etwas wirr.

    Meiner Meinung nach:
    - Global nur wenn nötig. Warum das nötig sein sollte, erschließt sich mir grade noch nicht ganz.

    - Default Konstruktoren nur wenn sinnvoll, sonst nicht bereit stellen.

    - Init Funktionen können Sinvoll sein, wenn's auf dem Stack sein soll/muss und zum Zeitpunkt der Konstruktion noch nicht alles fest steht, (z.B. weil es von User eingaben o.ä. abhängt). Eigentlich ziehe ich dafür dann aber Heap und entsprechende Konstruktoren vor.

    Ansonsten, Speicher auf Heap / Stack, Performance Optimierung etc. hängen gewaltig von der konkreten Anwendung ab. Es kann ja auch sein, dass die Anwendung viel Speicher braucht, da will man schon mal Speicher zwischendurch wieder frei geben.



  • Schlangenmensch schrieb:

    Irgendwie liest sich das für mich etwas wirr.

    Meiner Meinung nach:
    - Global nur wenn nötig. Warum das nötig sein sollte, erschließt sich mir grade noch nicht ganz.

    Von z.B. Windows ausgehend haben UI-Anwendungen Callback-Funktionen (Fensterprozedur). Da das Anwendungsobjekt der Ansprechpartner für eben diese Funktionen sein soll (z. B. die Ereignissbehandlung) dachte ich mir das es schlau wäre zumindest den Zeiger auf das Anwendungsobjekt global zu machen.

    Das es sich wirr liest liegt auch daran das ich so meinen Gedanken zur Projektrealisierung freien Lauf lasse. Ich jedoch wesentlichen Aspekten zur Umsetzung nicht zustimme, wie aus dem Kontext der Frage / Antworten hervorgeht 😃



  • Bist du der gleiche, der auch Framework design fürs Spiel gepostet hat?



  • Nein 😮



  • So, habe mich mal unter meinem registrierten Nick angemeldet 😉

    Das Projekt an dem ich arbeite, bekommt Daten von einem anderen Gerät (bzw. mehreren Geräten), diese werden verarbeitet und sollen auf einem Server gespeichert werden und auf dem Client sichtbar gemacht werden.
    Derzeit wird das Framework ausgehandelt. Ich kann bei Null beginnen...steht das Framework, übernehme ich die Windows-Implementierung. Dann verlasse ich das Projekt und "altgediente" Mitarbeiter übernehmen dann die Linux-Implementierung.

    Den Post über das "Spieleframedingsdesign" hatte ich auch gesehen.



  • hab mal ein bisschen vor mich hin gedacht:

    //#define BSS
    
    class App
    {
    public:
        virtual ~App() = default;
    
    public:
        virtual void Run() = 0;
    };
    
    class AppImpl: public App
    {
    public:
        AppImpl();
    
    public:
        virtual ~AppImpl();
    
    public:
        virtual void Run();
    };
    
    AppImpl::AppImpl()
    {
    
    }
    
    AppImpl::~AppImpl()
    {
    
    }
    
    void AppImpl::Run()
    {
    
    }
    
    #ifdef BSS
    namespace global{
        AppImpl* app_impl;
        App *app;
    }
    
    #else //DATA
    namespace global{
        AppImpl app_impl;
        App *app = &app_impl;
    }
    #endif
    
    int WINAPI WinMain(
                    HINSTANCE,
                    HINSTANCE,
                    LPSTR,
                    int)
    {
    #ifdef BSS
        global::app_impl = new AppImpl();
        global::app = global::app_impl;
    #endif
    
        global::app->Run();
    
    #ifdef BSS
        delete global::app_impl;
    #endif
    
        return EXIT_SUCCESS;
    }
    

    Wenn BSS nicht definiert ist dann werden der initialisierten Zeiger ins DATA Register geschrieben.

    global::app->Run();
      bf:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax        # c6 <WinMain+0x1f>
                            c2: R_X86_64_PC32       .data
      c6:   48 8b 00                mov    (%rax),%rax
      c9:   48 83 c0 10             add    $0x10,%rax
      cd:   48 8b 00                mov    (%rax),%rax
      d0:   48 8b 15 00 00 00 00    mov    0x0(%rip),%rdx        # d7 <WinMain+0x30>
                            d3: R_X86_64_PC32       .data
      d7:   48 89 d1                mov    %rdx,%rcx
      da:   ff d0                   callq  *%rax
    

    wenn allerdings BSS definiert ist, dann sind die Zeiger uninitilaisiert (bzw wenn sie NULL sind)

    global::app->Run();
      ef:   48 8b 05 08 00 00 00    mov    0x8(%rip),%rax        # fe <WinMain+0x57>
                            f2: R_X86_64_PC32       .bss
      f6:   48 8b 00                mov    (%rax),%rax
      f9:   48 83 c0 10             add    $0x10,%rax
      fd:   48 8b 00                mov    (%rax),%rax
     100:   48 8b 15 08 00 00 00    mov    0x8(%rip),%rdx        # 10f <WinMain+0x68>
                            103: R_X86_64_PC32      .bss
     107:   48 89 d1                mov    %rdx,%rcx
     10a:   ff d0                   callq  *%rax
    

    Liegt z. B. die fette App-Klasse z.B. irgendwo im Heap, dann werden die Zeiger über das DATA Register angesprochen. Das würde aber bedeuten das ich immer über die VTable gehe und über den Zeiger auf die App...
    In einer Loop habe ich also sehr viele Referenzierungen.

    Initialisiere ich das globale App-Objekt auch global, dann brauche ich wahrscheinlich die Init-Funktion...habe aber weniger Dereferenzierungen...
    wenn es darauf ankommt das der Code so schnell ist wie möglich


  • Mod

    FrankTheFox schrieb:

    Das würde aber bedeuten das ich immer über die VTable gehe und über den Zeiger auf die App...

    Weil der Compiler aus dem Typ des Zeigers nicht bereits beom Compilieren auf den dynamischen Typ des Objektes dahinter schließen kann, und daher nicht weiss, welcher Override der Funktion am Ende zuständig ist. Für diesen Zweck gibt es seit C++11 final.

    Mit der Frage, wo sich das Objekz befindet hat das nicht direkt etwas zu tun.

    class AppImpl: public App
    {
    ... 
    public:
        virtual void Run() final;
    };
    


  • Jo, stimmt. Mit Optimierungen seitens des Compilers ist dann auch der Vtable-Zugriff fast weg.

    Ich befürchte nur, hängt aber dann auch vom späteren Design ab das ich massig Dereferenzierungen habe und das dann Zeit kostet.

    eine "große" Application-Klasse bietet für mich jedoch Vorteile:

    - Ich kann alle Handles belegen.
    - Hier habe einen zentralen Datentopf.
    - RAII
    - Ich kann die EventLoop / MessageLoop verstecken und die Klasse
    Eventabhängig machen (z. B. sollte der User Eingaben vornehmen wollen 😉 )
    - ein zentrales "Cleanup"
    - OS abhängiges Zeugs ausführen.
    - Member als automatische Variablen

    Nachteile:

    - ein großer Speicherblock, der angefordert wird.
    - kann schnell unübersichtlich werden.


Anmelden zum Antworten