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 A
ist leer und wird nicht wirklich benötigt. Sollte es dort sein oder sollte es entfernt werden?
quelle
virtual ~base () = default;
Kompiliert beispielsweise nicht (aus gutem Grund)auto_ptr
.Antworten:
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.
quelle
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.
quelle
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
=default
sie 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).
quelle
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*c
hat eine ,+
die zwei Provisorien nimmt und einen temporären: Vermeidung von Klon und löschen kann sinnvoll)quelle
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:
Dies gilt nicht in Fällen, in denen Sie einen nicht standardmäßigen Kopierkonstruktor oder Zuweisungsoperator nur als Optimierung angeben.
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.
quelle
Es gibt einen weiteren Punkt, der in der Diskussion noch nicht erwähnt wurde: Ein Destruktor sollte immer virtuell sein.
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.
quelle