init Funktion oder Konstruktor



  • Hallo zusammen!

    Ich hätte gerne mal von euch eine Meinung gehört, wann es aus Design technischen Gründen Sinn macht für eine Klasse eine init Funktion zu schreiben und wann es besser ist diese Dinge im Konstruktor zu machen.

    Ich wäre dankbar für ein paar Erfahrungen eurerseits!

    Viele Grüße und vielen Dank
    Manni



  • bei nur einem Konstruktor würd ich es im Konstruktor schreiben.
    Eine init-Funktion macht dann Sinn, wenn man mehrere Konstruktoren hat oder das Objekt zu einem späteren Zeitpunkt wieder zurücksetzen können will



  • zwutz schrieb:

    Eine init-Funktion macht dann Sinn, wenn man mehrere Konstruktoren hat oder das Objekt zu einem späteren Zeitpunkt wieder zurücksetzen können will

    Und wenn gewisse Daten zum Zeitpunkt der Konstruktion noch nicht bekannt sind.



  • Manfredo schrieb:

    Ich wäre dankbar für ein paar Erfahrungen eurerseits!

    Ich liefer noch eine weitere Extremmeinung hinzu:

    Ich verwende immer Konstruktoren (und dabei die Initialisierungsliste), was leider bei mehreren Konstruktoren mehraufwand bedeutet (Anderseits bleibe ich so konsistenz bei der Verwendung).

    Wenn eine Initialisierung wirklich Stufenweise abläuft, mache ich die Konstruktoren privat, und biete statische Fabrikmethoden an. Die einzige Ausnahme die ich vielleicht verwenden würde, wäre für das Zurücksetzen des Zustandes (Sofern die Neukonstruktion zu teuer ist).

    Bislang habe ich aber, mindestens auf die letzten 5 Jahren gerechnet, mit Sicherheit keine Initfunktion abseits der Konstruktoren mehr geschrieben (Und verwendet auch nur, wenn vorhandener Code darauf aufgebaut hat, und von mir noch nicht umgebaut wurde).



  • Ich hasse init-Funktionen.

    Denn bisher, wenn ich darüber stolper sieht es so aus das sie
    a) nur im (einzigen) Konstruktor aufgerufen werden
    oder
    b) derjenige einfach nicht geschnallt hat das man Objekte auch löschen und neu anlegen kann



  • Auch wenn ich selber selten Init-Funktionen verwenden, aber eure Argumentation hört sich so an, als ob auch die Methode 'clear()' beim std::vector oder std::string etc. sinnlos wäre...

    Wenn man beispielsweise eine Liste von polymorphen Objekten hat, möchte man evtl. einzelne Objekte wieder zurücksetzen, ohne sie komplett neu erzeugen zu müssen und sie dann wieder der Liste hinzufügen müßte (da sich ja der Speicherplatz geändert hat).



  • Erst einmal sollte zwischen öffentlichen und privaten Init-Memberfunktionen unterschieden werden. Erstere sind meist dazu da, damit der Benutzer das Objekt nach der Konstruktion in einen gültigen Zustand versetzen kann. Und dieses Vorgehen finde ich im Allgemeinen schlecht, weil es die Verletzung von Klasseninvarianten geradezu in Kauf nimmt und bewährte Sprachmittel wie Konstruktoren, welche genau diese Probleme umschiffen, ignoriert.

    Anders sind interne Memberfunktionen, welche Codeduplizierung vermeiden, indem sie von den Konstruktoren aufgerufen werden. Gegen die spricht eigentlich nichts. Oft kommt man aber dennoch nicht darum herum, in mehreren Konstruktoren die gesamte Initialisierungsliste auszuführen, aber zumindest weitere Anweisungen im Konstruktorrumpf können so ausgelagert werden.



  • Nexus schrieb:

    Oft kommt man aber dennoch nicht darum herum, in mehreren Konstruktoren die gesamte Initialisierungsliste auszuführen

    das stimmt so nicht ganz - man kann die member noch mal in ein struct packen und diesem nen ctor verpassen - den man dann halt nur noch aufzurufen braucht, wie man lustig ist...

    bsp.:

    class foo
    {
      struct _m
      {
        int a;
        float b;
        int c;
    
        std::vector<int> d;
    
        _m(int a, float b, int c, std::vector<int> d)
        : a(a), b(b), c(c), d(d)
        {}
      } m;
    
    public:
      foo(int a, float b, int c)
      : m(a, b, c, std::vector<int>())
      {}
    
      foo(int a, std::vector<int> d)
      : m(a, 1., 0, d)
      }{
    };
    

    bb



  • Th69 schrieb:

    Auch wenn ich selber selten Init-Funktionen verwenden, aber eure Argumentation hört sich so an, als ob auch die Methode 'clear()' beim std::vector oder std::string etc. sinnlos wäre...

    Ja, warum auch nicht. Man könnte die clear-Funktion ja auch außerhalb der Klassen deklarieren:
    template<typename T> void clear( T& t ) {t=T();}
    . Ich hab nichts gegen die clear-Funktionen der std-Bibliothek, mich würde ihr Fehlen aber auch nicht stören. Und mich würde es nerven, wenn Klassen generell mit so einer clear-Funktion designt werden würden. Weil imho nicht von Mehrwert.

    (Mal die Speicher-Behaltungs-Optimierung außer Acht gelassen.)



  • unskilled schrieb:

    man kann die member noch mal in ein struct packen und diesem nen ctor verpassen - den man dann halt nur noch aufzurufen braucht, wie man lustig ist...

    Das stimmt natürlich. Aber viel gewonnen hast du dadurch nicht, weil du nach wie vor in der Initialisierungsliste Werte für alle Member angeben musst. Nur halt nebeneinander statt untereinander, hängt aber von deinem Stil ab.

    Was du nicht mehr tun musst, sind die Namen der Member explizit zu schreiben. Im Konstruktor sieht man nur noch die Werte. Allerdings kann gerade dies auch von Nachteil sein, weil man sich statt des Membernamens nun die Position in der Parameterliste merken muss.

    Mir gefällt auch der Gedanke nicht besonders, innerhalb der Klasse ständig _m.member schreiben zu müssen. Könnte man sich höchstens als Alternative zu Member-Prä- bzw. Postfixen überlegen. Mir scheint eine zusätzliche Klasse für den allgemeinen Fall jedoch etwas Overkill zu sein, aber es gibt sicher Anwendungsfälle, in denen diese Technik nützlich ist.



  • Badestrand_ schrieb:

    Th69 schrieb:

    Auch wenn ich selber selten Init-Funktionen verwenden, aber eure Argumentation hört sich so an, als ob auch die Methode 'clear()' beim std::vector oder std::string etc. sinnlos wäre...

    Ja, warum auch nicht. [...] Und mich würde es nerven, wenn Klassen generell mit so einer clear-Funktion designt werden würden. Weil imho nicht von Mehrwert.

    Natürlich, der Unterschied von Containern und irgendwelchen Klassen ist aber schon wichtig. Der einzige Lebensinhalt von Containerklassen besteht in der Speicherung und Verwaltung von Objekten. Da ist es meiner Meinung nach durchaus angebracht, sämtliche Objekte auf einmal zu löschen. Schliesslich sind diese eigenständig und haben nicht direkt mit dem Status des Containers zu tun. Zumindest ist die Kopplung Container <-> Element viel geringer als Klasse <-> Membervariable. Ob freie Funktion oder Member ist wieder eine andere Frage, spielt aber im Bezug auf die bereitgestellte Funktionalität keine entscheidende Rolle.

    Im Weiteren wird bei Containerklassen beim Aufruf von clear() tatsächlich eine Leerung durchgeführt. Es existieren nach der Operation meistens weniger Elemente als vorher. Ein Objekt selbst hingegen kann man nicht leeren, denn man kann eine Entität an sich nicht löschen. Die Löschung geschieht immer im Bezug auf die Umgebung, welche das eigentliche Objekt referenziert – sei es ein Zeiger bei delete , ein Container bei erase() , oder irgendwas (das gilt im Übrigen nicht nur für C++).

    Genau aus diesem Grund ist ein allgemeines clear() sinnlos. Was sollte es tun? Das Objekt in den Ausgangszustand versetzen? Das ist oft gar nicht möglich, ausserdem sieht C++ für die Überschreibung des gesamten Objekts Zuweisungsoperatoren vor. Bei Containern sieht es jedoch wie erwähnt anders aus.


  • Administrator

    Th69 schrieb:

    Auch wenn ich selber selten Init-Funktionen verwenden, aber eure Argumentation hört sich so an, als ob auch die Methode 'clear()' beim std::vector oder std::string etc. sinnlos wäre...

    Was hat bitte die clear Funktion mit einer init Funktion zu tun? Das sind doch zwei ganz verschiedene paar Schuhe. Das ist als wenn du push_back mit init vergleichen würdest. Ich kann jedenfalls keinen Zusammenhang erkennen.

    Ich bin auch gegen öffentliche init Funktionen. Dafür ist der Konstruktor da. Es ist höchstens zum Teil die Frage da, ab wann ein Objekt initialisiert ist? Typisches Beispiel hierfür sehe ich bei boost::thread . Nach der Konstruktion des Objektes startet der Thread bereits. Da hätte ich nichts gegen eine launch oder start Methode einzuwenden gehabt, da ich dies nicht mehr als zur Initialisierung gehörend empfinde.

    Grüssli



  • Bei komplexen Objekten, die Function-Calls machen müssen, um in einem benutzbaren Zustand zu gelangen, verwende ich immer init-Funktionen und ich verwende meist ein HRESULT als Rückgabewert, der dann auch geprüft wird.
    Im Konstruktor setzte ich meist alle Membervar's auf NULL bzw. 0.

    Konstruktoren haben keine Rückgabewerte. Daher ist es bescheuert in Konstruktoren Function-Calls zu machen. Scheitert ein solcher Fucntion-Call, dann gibt es nur noch die Möglichkeit eine Exception ausm Konstruktor zu werfen.

    Init-Funktionen sind bei mir immer public, da sie eben wegen der Rückgabeproblematik nicht aus dem Konstruktor heraus aufgerufen werden können.

    Ich machs meist so:

    bool MyFunction()
    {
       MyObject1 obj1;
       if(FAILED(obj1.init())
       {
          logger.log("Fehlermeldung xxxx");
          return false;
       }
    
       MyObject2 obj2;
       if(FAILED(obj2.init())
       {
          logger.log("Fehlermeldung yyyy");
          return false;
       }
    
       return true;
    }
    

    FAILED ist ein Macro, dass prüft, ob ein HRESULT "ok" ist. So reicht ein Blick in die log-Datei und ich weiß, wo ein init() fehlgeschlagen ist.



  • Dravere schrieb:

    Ich bin auch gegen öffentliche init Funktionen. Dafür ist der Konstruktor da. Es ist höchstens zum Teil die Frage da, ab wann ein Objekt initialisiert ist? Typisches Beispiel hierfür sehe ich bei boost::thread . Nach der Konstruktion des Objektes startet der Thread bereits. Da hätte ich nichts gegen eine launch oder start Methode einzuwenden gehabt, da ich dies nicht mehr als zur Initialisierung gehörend empfinde.

    Sehe ich auch so. Für mich ist ein Objekt initialisiert, wenn die Invarianz gegeben ist und das Objekt korrekt benutzt werden kann.
    In dem Sinne wäre ich auch eher dafür, dass boost::thread nicht gleich im ctor starten würde, sondern das nur als Überladung anbietet, um den in manchen Fällen unnötigen launch zu vermeiden. (Eigentlich so ähnlich, wie bei std::ifstream .
    Vor allem finde ich, dass Klassen eine Möglichkeit anbieten sollten, dass man ein Objekt auf den Zustand, welches es gerade nach der Initialisierung hatte zurückzusetzen. (also bei thread auch einen thread starten, ohne das Objekt zerstören zu müssen).
    Leider geht das nicht immer so einfach.

    @Hans_Guck_In_Die_Luft
    Ich denke, dass du zu fest in C verwurzelt bist und den Sinn von Exceptions nicht so ganz begriffen hast.
    Dein Beispiel kann mit exception viel schöner und ohne Fehlerbehandlung in eigentlichem Produktivcode geschrieben werden:

    void MyFunction()
    {
       MyObject1 obj1;
       MyObject2 obj2;
    
       // das funktioniert hier sicher richtig, weil obj1 und obj2 
       // bestimmt korrekt erzeugt hätten werden können.
       // Bei dir kann man das nicht so einfach machen..
       obj2.foo ( obj1 );
    }
    

    Wenn du allerdings trotzdem etwas auf einen Fehler reagieren kannst, ohne den Standardhandler zu benutzen, dann kannst du das dennoch tun:

    void MyFunction()
    {
       try
       {
         MyObject1 obj1;
         MyObject2 obj2;
    
         obj2.foo ( obj1 );
       }
       catch ( /* je nach dem*/ )
       {
         // ok, ein Objekt konnte nicht erstellt werden, also machen wir 
         // was anderes
       }
    }
    

    Alleine schon das umschreiben der Funktion sieht am Ende besser aus, als deine Variante:

    bool MyFunction()
    {
       try
       {
         MyObject1 obj1;
         MyObject2 obj2;
       }
       catch (..)
       {
          logger.log("Fehlermeldung xxxx");
          return false;
       }
       return true; // ok, scheint alles in Ordnung gewesen zu sein
    }
    

    Und jetzt sag mir, dass das in einer Funktion, die nicht nur 2 Zeilen eigentlichen Code hat, sondern vielleicht 10-20 nicht übersichtlicher wird.



  • @drakon:

    Dein Ansatz führt zum inflationären Gebrauch von Exceptions. Jede Klasse, deren
    Instanzen Function-Calls machen müssen, um benutzbar zu sein, muss dann
    Exceptions werfen. Das ist meiner Meinung nach sicher nicht der Sinn von
    Exceptions. Dafür hat Gott return-values geschaffen.

    Ein weiteres Argument für init() Methoden ist, dass der this-Pointer erst gültig
    ist, wenn der Konstruktor verlassen wurde. In nem Konstruktor sollte man nie
    einen Function-Call mit "this" als Parameter machen.

    In ner init() Methode geht das aber sehr wohl.


  • Mod

    Hans_Guck_In_Die_Luft schrieb:

    @drakon:

    Dein Ansatz führt zum inflationären Gebrauch von Exceptions. Jede Klasse, deren
    Instanzen Function-Calls machen müssen, um benutzbar zu sein, muss dann
    Exceptions werfen. Das ist meiner Meinung nach sicher nicht der Sinn von
    Exceptions.

    Warum?

    Hans_Guck_In_Die_Luft schrieb:

    Ein weiteres Argument für init() Methoden ist, dass der this-Pointer erst gültig
    ist, wenn der Konstruktor verlassen wurde. In nem Konstruktor sollte man nie
    einen Function-Call mit "this" als Parameter machen.

    Unfug.

    Hans_Guck_In_Die_Luft schrieb:

    In ner init() Methode geht das aber sehr wohl.

    Nur dass du this dann immer noch auf ein nicht richtig initialisiertes Objekt zeigt und sich die aufgerufene Funktion nicht auf die Einhaltung der Objektinvarianzen verlassen kann. Ich sehe hier keinen Unterschied zum Aufruf aus Konstruktoren heraus.



  • Hans_Guck_In_Die_Luft schrieb:

    Ein weiteres Argument für init() Methoden ist, dass der this-Pointer erst gültig
    ist, wenn der Konstruktor verlassen wurde. In nem Konstruktor sollte man nie
    einen Function-Call mit "this" als Parameter machen.

    Camper hats bereits gesagt, dass das Unfug ist. Wenn das stimmen würde, würde das heissen, dass man auch keine Memberfunktionen aus dem Konstruktor aufrufen darf und dann wäre so ziemlich die komplette Standardbibliothek falsch oder was auch immer du meinst das das ist..

    Es bestehen gefahren, wie wenn du z.B über den this Zeiger (natürlich auch sonst) auf ein Objekt zugreifst, dass noch nicht initialisiert wurde, daher ist der Gebrauch des this Zeigers micht Vorsicht zu geniesen ( vor allem in der Initialisierungsliste), abre das rührt hauptsächlich daher, dass das Objekt, dass den Zeiger benutzt ev. meint, dass das Objekt bereits besteht. Aber den Zeiger einfach speichern ist kein Problem.
    Ein zweites Problem könnte sein, dass der this Zeiger gebraucht werden könnte, um (auch wieder unvorsichtigerweise) einen polymorphen Funktionsaufruf zu machen und das kann ebenfalls in die Hose gehen. (wird es auch).

    Der Konstruktor ist dazu da, um die Invarianz zu erstellen und ggf. um ein paar Parameter zu setzen und Berechnungen zu machen, welche sonst über Memberfunktionen gleich nach der Erstelltung aufgerufen werde müssten. (also rein syntax sugar).



  • Ok, die Wahrheit liegt in der Mitte:

    http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.7

    this im C'tor benutzen geht manchmal. Manchmal isses gefährlich. Meiner Meinung kann man einfach drauf verzichten. Habe noch nie Code von guten Leuten gesehen, die das machen.

    @drakon:

    Dein Ansatz führt zum inflationären Gebrauch von Exceptions. Jede Klasse, deren
    Instanzen Function-Calls machen müssen, um benutzbar zu sein, muss dann
    Exceptions werfen. Das ist meiner Meinung nach sicher nicht der Sinn von
    Exceptions.
    Warum?

    Exceptions sind Ausnahmen. Willste jedes mal, wenn ein Pointer, der NULL ist,
    jedoch nicht NULL sein darf, ne Exception werfen? Exceptions sind meiner
    Meinung nach für Sachen, falls man kein File-Handle bekommt, die Socket-
    Connection nicht funzt oder der Drucker nicht antwortet. Also für Sachen, deren
    Ursache außerhalb der eigentlichen Applikation liegt. Für interene "Fehler" wie
    z.B. Mutex ist blockiert oder eigene Invarianten sind verletzt, kann man einfach Rückgabewerte verwenden.


  • Administrator

    Hans_Guck_In_Die_Luft schrieb:

    Willste jedes mal, wenn ein Pointer, der NULL ist, jedoch nicht NULL sein darf, ne Exception werfen?

    Wieso erlaubst du dann überhaupt erst die Übergabe von Zeigern? Dann kannst du auch eine Referenz verlangen. Und schon musst du diese Überprüfung nicht mehr machen.
    Finde es aber sehr interessant, dass du dies als Argument bringst. Lässt mich auch sehr vermuten, dass du viel zu stark in C denkst.

    Es ist zudem auch die Aufgabe des Programmierers, richtige Werte zu prüfen, welche an den Konstruktor übergeben werden. Wenn dann trotzdem falsche Werte übergeben wurden, dann ist eine Exception gerechtfertigt. Womöglich sogar ein assert .

    Wenn die Konstruktion eines Objektes schief läuft, dann ist wirklich etwas sehr schief gelaufen, was eine Exception völlig rechtfertigt.

    Grüssli



  • Hans_Guck_In_Die_Luft schrieb:

    Ok, die Wahrheit liegt in der Mitte:

    http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.7

    this im C'tor benutzen geht manchmal. Manchmal isses gefährlich. Meiner Meinung kann man einfach drauf verzichten. Habe noch nie Code von guten Leuten gesehen, die das machen.

    Das ist ja genau das, was ich geschrieben habe.
    Ob man this braucht oder nicht kommt halt drauf an. Implizit brauchen den sehr viele. Explizit eher selten. Ich brauche die explizite Variante eigentlich nur an einem Ort, wo sich ein Objekt selbst bei einem Singleton registriert und dann im Destruktor wieder abmeldet. Da finde ich das noch ganz praktisch this im Konstruktor zu benutzen.

    Exceptions sind Ausnahmen. Willste jedes mal, wenn ein Pointer, der NULL ist,
    jedoch nicht NULL sein darf, ne Exception werfen?

    Wenn irgendwo, wo ein Zeiger nicht 0 sein darf 0 ist, dann ja. Wenn du da mal in üblichen Code schaust, dann wirst du sehen, dass da ein assert ist. Ich würde eine Version bevorzugen, die eine Exception wirft. Dann kann man da nämlich ein paar Dinge auch noch im Release Mode stehen lassen und kriegt allenfalls eine gute Meldung und nicht einfach nur einen Absturz.

    Ich stelle mal (wieder) die Brück zu Eiffel, wo Invarianten und Preconditions (und auch Postconditions) tief in der Sprache verwurzelt sind.
    Wenn eine Invariante oder aber auch Precondition nicht eingehalten wird, dann sollte das dem Programmierer an den Kopf springen so, dass er das nicht ignorieren kann. Und da sind Exceptions einge gute Möglichkeit.


Anmelden zum Antworten