Best practice Default Constructors / operators



  • Moin zusammen

    ich saniere gerade einige ältere APIs mit diversen Klassen und bringe sie auf den neuesten Stand.
    Dabei rüste ich ebenfalls Doxygen-comments nach.
    Da es recht viele Klassen sind, die ebenfalls noch dokumentiert werden müssen, dachte ich mir:

    Was nicht da ist, muss ich nicht dokumentieren.
    Sprich: Wenn ich den Compiler soviel wie möglich auto-generieren lasse, spart mir das Arbeit.

    Daher die Frage:
    Sind diese expliziten Definitionen von Konstruktoren / Operatoren notwendig?

    1. Konstruktoren / Destruktoren
      Kann ich mir die Angabe sparen, bzw. ist das übliche Praxis, die Default Konstruktoren anzugeben, oder eben wegzulassen?
    MyClass() = default;
    ~MyClass() = default;
    
    1. Copy/Move-Assignment-Operatoren ( und Konstruktoren )
      Definition von Default-Operatoren/Konstruktoren sinnvoll oder nicht?
    MyClass &operator=( MyClass &&other ) = default;
    
    1. Eigene Implementation von Operator/Konstruktor vs. Autogenerierung?
    MyClass &operator=( MyClass &&other )
    {
       Member = std::move( other.Member );
       return *this;
    }
    

    vs.

    MyClass &operator=( MyClass &&other ) = default;
    

    Wie verfährt der Compiler hier mit trivialen Integertypen? Diese kopiere ich meist nur anstatt sie zu moven. Was würde der Compiler in dem Fall tun?

    Was ist in diesen Fällen "Best Practice"? Gibt es Risiken, wenn man dem Compiler die Autogenerierung überlässt?


  • Mod

    Erst einmal grundsätzliches: Das alles brauchst du dann und nur dann, wenn deine Klassen irgendeine Form von nicht-trivialer Ressourcenverwaltung macht. Ist das nicht der Fall, dann brauchst du nichts zu machen. Das beantwortet auch deine Frage nach den trivialen Integern, denn bei denen gibt es ja gar keine Ressourcen zu moven, da stellt sich die Frage gar nicht. Bei solchen Fällen erzeugt der Compiler automatisch passende Defaults, die auch sinnvoll sind, denn sie tun schließlich nichts, weil es nichts zu tun gibt. Und du musst das entsprechend auch nicht im Code haben.

    Falls du aber einen Fall hast, wo du eine Klasse hast, die eine nicht-triviale Implementierung braucht, gibt es ja die berühmte Rule of 3 bzw. Rule of 5, die besagt, wenn du eine dieser Methoden nicht-trivial implementieren musst, dann musst du ziemlich sicher auch alle 3/5 dieser Methoden selber implementieren, um Korrektheit deines Programms zu garantieren (und wichtiger ist halt die Rule of 0, die mein Paragraph oben ist, und sagt, dass du das eigentlich so gut wie nie brauchst und gar nichts tun musst/solltest). Wenn und nur wenn du in einem solchen Fall doch möchtest, dass für eine dieser speziellen Methoden trotzdem die default-Implementierung möchtest (auch wenn es wahrscheinlich nicht richtig sein wird), kannst du das mit =default erreichen. Denn ansonsten erzeugt der Compiler diese Methoden nicht mehr automatisch, sobald eine der Methoden explizit implementiert wurde. Die genauen Regeln sind kompliziert, aber besonders die automatischen Moves verschwinden beim kleinsten Anzeichen von Gefahr.

    Eine explizite Definition als =default gilt übrigens auch selbst eine solche nutzergegebene Implementierung, d.h. es hat Nachteile, einfach zufällig einzelne Methoden grundlos als =default zu definieren, weil dann eben die anderen Methoden eventuell gar nicht mehr automatisch erzeugt werden (besonders die Moves), was man wahrscheinlich nicht will. Und wenn man das will, sollte man lieber die Methoden =deleteen, die man nicht möchte.

    Jetzt sagst du jetzt vielleicht: Was ist mit virtuellen Klassen, brauchen die nicht einen expliziten virtuellen Destruktor? Blockt der dann nicht auch die automatische Erzeugung der Moves? Richtig! Und da ist dann eine gute und sinnvolle Anwendung von =default, wo du bei der virtuellen Basisklasse den Destruktor selbst definierst, und dann alle anderen Methoden als =default definierst (aber nicht bevor du über Slicing gelesen hast!)

    Überhaupt beantworten die Cpp Core Guidelines alle deine Fragen viel genauer, als ich das hier kann, und sind wohl die Best Practices:
    https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#S-ctor



  • @SeppJ sagte in Best practice Default Constructors / operators:

    Die genauen Regeln sind kompliziert, aber besonders die automatischen Moves verschwinden beim kleinsten Anzeichen von Gefahr.

    Das ist eigentlich auch so mit die größte Befürchtung die ich habe. Dass der Compiler etwas generiert, aber da ein Member keinen Move erlaubt, in Wirklichkeit kopiert wird, obwohl ich davon ausgehe das gemoved wird.
    Prinzipiell arbeite ich immer sauber mit "=delete;" d.h. Operatoren/etc. die ich nicht benötige, sind auch nicht da. Wenn ich das konsequent mache sollte ich theoretisch auch keine Überraschungen erleben, oder?

    PS: danke für den Link. Ich werde das mal lesen.



  • @It0101 sagte in Best practice Default Constructors / operators:

    Prinzipiell arbeite ich immer sauber mit "=delete;" d.h. Operatoren/etc. die ich nicht benötige, sind auch nicht da. Wenn ich das konsequent mache sollte ich theoretisch auch keine Überraschungen erleben, oder?

    Lies das C.21: If you define or =delete any copy, move, or destructor function, define or =delete them all aus dem Link von @SeppJ dazu.

    Declaring any copy/move/destructor function, even as =default or =delete, will suppress the implicit declaration of a move constructor and move assignment operator. Declaring a move constructor or move assignment operator, even as =default or =delete, will cause an implicitly generated copy constructor or implicitly generated copy assignment operator to be defined as deleted.



  • @It0101 sagte in Best practice Default Constructors / operators:

    MyClass() = default;
    

    Zu dem Default-Konstruktor sollte man natürlich noch anmerken, dass der auch häufiger abseits von Ressourcenverwaltung Sinn macht, wenn es sowohl einen benutzterdefinierten Konstruktor gibt und gleichzeitig ein trivialer Default-Konstruktor gewünscht ist:

    struct Integer {
        Integer() = default;
        Integer(int value) :  value{ value } {}
        int value;
    };
    

    Ansonsten kann ich deine Sorgen gut verstehen, es kann natürlich immer einen obskuren Grund* geben, weshalb die Funktionen explizit default sind. Genau so gut kann das aber auch jemand geschrieben haben, der nicht genau wusste, was er tat. Gerade bei sowas sind gute Kommentare wichtig - nicht jeder Code ist intuitiv selbsterklärend.

    Schau dir auf jeden Fall mal genau an, wie die Klassen im restlichen Code verwendet werden, vielleicht hilft das ja deine Zweifel zu zerstreuen oder mögliche Gründe zu finden, weshalb man die Defaults haben wollte.

    * Hier wurde mal so was "obskures" diskutiert. Ein deleted Move-Konstrukor wurde für Overload Resolution herangezogen, obwohl ein Copy-Kostruktor existierte, was zu einem Fehler führte. Eine mögliche Lösung war einen default Move-Konstruktor zu deklarieren und diesen dann implizit zu löschen. Das dürfte aber ebenfalls unter den Themenkomplex "Ressourcenverwaltung" fallen.


Log in to reply