Die Regel 5 - es zu benutzen oder nicht?

20

Die Regel von 3 ( die Regel von 5 im neuen c ++ - Standard) lautet:

Wenn Sie den Destruktor, den Kopierkonstruktor oder den Kopierzuweisungsoperator selbst explizit deklarieren müssen, müssen Sie wahrscheinlich alle drei explizit deklarieren.

Andererseits rät der " Clean Code " von Martin , alle leeren Konstruktoren und Destruktoren zu entfernen (Seite 293, G12: Clutter ):

Was nützt ein Standardkonstruktor ohne Implementierung? Alles, was dazu dient, ist, den Code mit bedeutungslosen Artefakten zu überladen.

Wie geht man mit diesen beiden gegensätzlichen Meinungen um? Sollten leere Konstruktoren / Destruktoren wirklich implementiert werden?


Das nächste Beispiel zeigt genau, was ich meine:

#include <iostream>
#include <memory>

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    ~A(){}
    A( const A & other ) : v( new int( *other.v ) ) {}
    A& operator=( const A & other )
    {
        v.reset( new int( *other.v ) );
        return *this;
    }

    std::auto_ptr< int > v;
};
int main()
{
    const A a( 55 );
    std::cout<< "a value = " << *a.v << std::endl;
    A b(a);
    std::cout<< "b value = " << *b.v << std::endl;
    const A c(11);
    std::cout<< "c value = " << *c.v << std::endl;
    b = c;
    std::cout<< "b new value = " << *b.v << std::endl;
}

Kompiliert einwandfrei mit g ++ 4.6.1 mit:

g++ -std=c++0x -Wall -Wextra -pedantic example.cpp

Der Destruktor für struct Aist leer und wird nicht wirklich benötigt. Sollte es dort sein oder sollte es entfernt werden?

BЈовић
quelle
15
Die 2 Zitate sprechen über verschiedene Dinge. Oder ich vermisse deinen Standpunkt total.
Benjamin Bannier
1
@honk Im Kodierungsstandard meines Teams haben wir die Regel, immer alle 4 zu deklarieren (Konstruktor, Destruktor, Kopierkonstruktor). Ich habe mich gefragt, ob das wirklich Sinn macht. Muss ich Destruktoren wirklich immer deklarieren, auch wenn sie leer sind?
BЈовић
Denken Sie bei leeren Desktruktoren darüber nach: codesynthesis.com/~boris/blog/2012/04/04/… . Ansonsten macht die Regel von 3 (5) für mich vollkommen Sinn, keine Ahnung, warum man eine Regel von 4 haben möchte.
Benjamin Bannier
@honk Achten Sie auf Informationen, die Sie im Internet finden. Nicht alles ist wahr. virtual ~base () = default;Kompiliert beispielsweise nicht (aus gutem Grund)
BЈовић
@VJovic, Nein, Sie müssen keinen leeren Destruktor deklarieren, es sei denn, Sie müssen ihn virtuell machen. Und während wir uns mit dem Thema befassen, sollten Sie es auch nicht verwenden auto_ptr.
Dima

Antworten:

44

Zunächst lautet die Regel "wahrscheinlich", sie gilt also nicht immer.

Der zweite Punkt, den ich hier sehe, ist, dass Sie, wenn Sie einen der drei deklarieren müssen, etwas Besonderes tun, wie das Zuweisen von Speicher. In diesem Fall wären die anderen nicht leer, da sie dieselbe Aufgabe ausführen müssten (z. B. den Inhalt des dynamisch zugewiesenen Speichers im Kopierkonstruktor kopieren oder diesen Speicher freigeben).

Als Fazit sollten Sie also keine leeren Konstruktoren oder Destruktoren deklarieren, aber es ist sehr wahrscheinlich, dass, wenn einer benötigt wird, auch die anderen benötigt werden.

Zum Beispiel: In einem solchen Fall können Sie den Destruktor weglassen. Es tut offensichtlich nichts. Die Verwendung intelligenter Zeiger ist ein perfektes Beispiel dafür, wo und warum die Regel 3 nicht gilt.

Es ist nur eine Anleitung, wo Sie einen zweiten Blick auf Ihren Code werfen können, falls Sie vergessen haben, wichtige Funktionen zu implementieren, die Sie sonst möglicherweise übersehen hätten.

thorsten müller
quelle
Bei Verwendung von intelligenten Zeigern sind die Destruktoren in den meisten Fällen leer (ich würde sagen,> 99% der Destruktoren in meiner Codebasis sind leer, da fast jede Klasse das Pimpl-Idiom verwendet).
BЈовић
Wow, das ist so viel Pimpling, dass ich es als stinkend bezeichnen würde. Bei vielen Compilern ist die Optimierung von Pimpled schwieriger (z. B. schwerer inline).
Benjamin Bannier
@honk Was meinst du mit "viele Compiler pickelten"? :)
BЈовић
@ VJovic: Entschuldigung, Tippfehler: 'Pimpled Code'
Benjamin Bannier
4

Hier gibt es wirklich keinen Widerspruch. Die Regel von 3 spricht über den Destruktor, den Kopierkonstruktor und den Kopierzuweisungsoperator. Onkel Bob spricht über leere Standardkonstruktoren.

Wenn Sie einen Destruktor benötigen, enthält Ihre Klasse wahrscheinlich Zeiger auf dynamisch zugewiesenen Speicher, und Sie möchten wahrscheinlich einen Kopier-Ctor und einen operator=(), der eine tiefe Kopie erstellt. Dies ist völlig orthogonal dazu, ob Sie einen Standardkonstruktor benötigen oder nicht.

Beachten Sie auch, dass es in C ++ Situationen gibt, in denen Sie einen Standardkonstruktor benötigen, auch wenn dieser leer ist. Angenommen, Ihre Klasse verfügt über einen nicht standardmäßigen Konstruktor. In diesem Fall generiert der Compiler keinen Standardkonstruktor für Sie. Das bedeutet, dass Objekte dieser Klasse nicht in AWL-Containern gespeichert werden können, da diese Container erwarten, dass die Objekte standardmäßig konstruierbar sind.

Wenn Sie jedoch nicht vorhaben, die Objekte Ihrer Klasse jemals in STL-Container zu packen, ist ein leerer Standardkonstruktor mit Sicherheit nutzlos.

Dima
quelle
2

Hier hat Ihr potenzielles (*) Äquivalent zum Standardkonstruktor / Zuweisung / Destruktor einen Zweck: Dokumentieren Sie die Tatsache, die Sie über das Problem haben, und stellen Sie fest, dass das Standardverhalten korrekt war. Übrigens haben sich die Dinge in C ++ 11 nicht ausreichend stabilisiert, um zu wissen, ob =defaultsie diesem Zweck dienen können.

(Es gibt noch einen weiteren möglichen Zweck: Geben Sie anstelle der Standard-Inline-Definition eine Out-of-Line-Definition an, die Sie besser explizit dokumentieren können, wenn Sie einen Grund dafür haben.)

(*) Potenzial, weil ich mich nicht an einen realen Fall erinnere, in dem die Dreierregel nicht galt. Wenn ich etwas in einem tun musste, musste ich etwas in den anderen tun.


Bearbeiten Sie nach dem Hinzufügen eines Beispiels. Ihr Beispiel mit auto_ptr ist interessant. Sie verwenden einen intelligenten Zeiger, aber keinen, der für den Job geeignet ist. Ich schreibe lieber einen, der - besonders wenn die Situation häufig auftritt - als das tut, was Sie getan haben. (Wenn ich mich nicht irre, bieten weder der Standard noch der Boost einen).

Ein Programmierer
quelle
Das Beispiel zeigt meinen Standpunkt. Der Destruktor wird nicht wirklich benötigt, aber die Regel von 3 besagt, dass er da sein sollte.
BЈовић
1

Die Regel 5 ist eine vorsichtige Erweiterung der Regel 3, die ein vorsichtiges Verhalten gegen möglichen Objektmissbrauch darstellt.

Wenn Sie einen Destruktor benötigen, bedeutet dies, dass Sie ein anderes "Ressourcen-Management" als das Standard-Management durchgeführt haben (nur Werte erstellen und zerstören ).

Da standardmäßig Werte kopiert, zugewiesen, verschoben und übertragen werden , müssen Sie definieren, was zu tun ist , wenn Sie nicht nur Werte halten.

Das heißt, C ++ löscht die Kopie, wenn Sie die Verschiebung definieren, und löscht die Verschiebung, wenn Sie die Kopie definieren. In den meisten Fällen müssen Sie definieren, ob Sie einen Wert emulieren möchten (also die Ressource kopieren und klonen und verschieben hat keinen Sinn) oder einen Ressourcenmanager (also die Ressource verschieben, bei der Kopieren keinen Sinn hat: die Regel von 3 wird die Regel der anderen 3 )

Die Fälle, in denen Sie sowohl Kopieren als auch Verschieben definieren müssen (Regel von 5), sind sehr selten: In der Regel haben Sie einen "großen Wert", der kopiert werden muss, wenn er bestimmten Objekten zugewiesen wird, aber verschoben werden kann, wenn er einem temporären Objekt entnommen wird (Vermeidung) ein Klon dann zerstören ). Dies ist bei AWL-Containern oder Rechencontainern der Fall.

Ein Fall kann Matrizes sein: sie zu unterstützen kopieren, weil sie sind Werte, ( a=b; c=b; a*=2; b*=3;dürfen einander nicht beeinflussen) , aber sie können auch bewegt durch die Unterstützung optimiert werden ( a = 3*b+4*chat eine , +die zwei Provisorien nimmt und einen temporären: Vermeidung von Klon und löschen kann sinnvoll)

Emilio Garavaglia
quelle
1

Ich bevorzuge eine andere Formulierung der Dreierregel, die vernünftiger erscheint: "Wenn Ihre Klasse einen Destruktor (außer einem leeren virtuellen Destruktor) benötigt, benötigt sie wahrscheinlich auch einen Kopierkonstruktor und einen Zuweisungsoperator."

Die Angabe als Einbahnstraßenbeziehung vom Destruktor aus macht einige Dinge klarer:

  1. Dies gilt nicht in Fällen, in denen Sie einen nicht standardmäßigen Kopierkonstruktor oder Zuweisungsoperator nur als Optimierung angeben.

  2. Der Grund für die Regel ist, dass der Standard-Kopierkonstruktor oder Zuweisungsoperator die manuelle Ressourcenverwaltung vermasseln kann. Wenn Sie Ressourcen manuell verwalten, haben Sie wahrscheinlich erkannt, dass Sie einen Destruktor benötigen, um sie freizugeben.

Jules
quelle
-3

Es gibt einen weiteren Punkt, der in der Diskussion noch nicht erwähnt wurde: Ein Destruktor sollte immer virtuell sein.

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    virtual ~A(){}
    ...
}

Der Konstruktor muss in der Basisklasse als virtuell deklariert werden, damit er auch in allen abgeleiteten Klassen virtuell wird. Selbst wenn Ihre Basisklasse keinen Destruktor benötigt, deklarieren und implementieren Sie einen leeren Destruktor.

Wenn Sie alle Warnungen auf (-Wall -Wextra -Weffc ++) setzen, warnt Sie g ++ davor. Ich halte es für eine gute Praxis, immer einen virtuellen Destruktor in einer Klasse zu deklarieren, da Sie nie wissen, ob Ihre Klasse letztendlich eine Basisklasse wird. Wenn der virtuelle Destruktor nicht benötigt wird, schadet er nicht. Wenn dies der Fall ist, sparen Sie Zeit, um den Fehler zu finden.

Lexi
quelle
1
Aber ich will den virtuellen Konstruktor nicht. Wenn ich das tue, würde jeder Aufruf einer Methode den virtuellen Versand verwenden. Nehmen Sie übrigens zur Kenntnis, dass es in c ++ keinen "virtuellen Konstruktor" gibt. Außerdem habe ich das Beispiel als sehr hohe Warnstufe zusammengestellt.
BЈовић
IIRC, die Regel, die gcc für seine Warnung verwendet, und die Regel, die ich im Allgemeinen ohnehin befolge, lautet, dass es einen virtuellen Destruktor geben sollte, wenn andere virtuelle Methoden in der Klasse vorhanden sind.
Jules