Sollte ich Zeiger oder Referenzen in Mitgliedsdaten bevorzugen?

138

Dies ist ein vereinfachtes Beispiel zur Veranschaulichung der Frage:

class A {};

class B
{
    B(A& a) : a(a) {}
    A& a;
};

class C
{
    C() : b(a) {} 
    A a;
    B b; 
};

Also ist B für die Aktualisierung eines Teils von C verantwortlich. Ich habe den Code durch Lint geführt und es ging um das Referenzmitglied: Lint # 1725 . Hier geht es darum, auf Standardkopien und -zuweisungen zu achten, was fair genug ist, aber Standardkopien und -zuweisungen sind auch mit Zeigern schlecht, sodass es dort wenig Vorteile gibt.

Ich versuche immer, Referenzen zu verwenden, wo ich kann, da nackte Zeiger unsicher darüber einführen, wer für das Löschen dieses Zeigers verantwortlich ist. Ich bevorzuge es, Objekte nach Wert einzubetten, aber wenn ich einen Zeiger benötige, verwende ich auto_ptr in den Mitgliedsdaten der Klasse, der der Zeiger gehört, und gebe das Objekt als Referenz weiter.

Ich würde im Allgemeinen nur dann einen Zeiger in Mitgliedsdaten verwenden, wenn der Zeiger null sein oder sich ändern könnte. Gibt es andere Gründe, Zeiger gegenüber Referenzen für Datenelemente vorzuziehen?

Stimmt es, dass ein Objekt, das eine Referenz enthält, nicht zuweisbar sein sollte, da eine Referenz nach der Initialisierung nicht geändert werden sollte?

markh44
quelle
"aber die Standardkopie und -zuweisung ist auch bei Zeigern schlecht": Dies ist nicht dasselbe: Ein Zeiger kann jederzeit geändert werden, wenn er nicht const ist. Eine Referenz ist normalerweise immer const! (Sie werden dies sehen, wenn Sie versuchen, Ihr Mitglied in "A & const a;" zu ändern. Der Compiler (mindestens GCC) warnt Sie, dass die Referenz auch ohne das Schlüsselwort "const" ohnehin const ist.
mmmmmmmm
Das Hauptproblem dabei ist, dass wenn jemand B b (A ()) macht, Sie geschraubt werden, weil Sie am Ende eine baumelnde Referenz haben.
Log0

Antworten:

68

Vermeiden Sie Referenzmitglieder, da sie die Möglichkeiten der Implementierung einer Klasse einschränken (einschließlich, wie Sie bereits erwähnt haben, die Implementierung eines Zuweisungsoperators verhindern) und keine Vorteile für das bieten, was die Klasse bieten kann.

Beispielprobleme:

  • Sie sind gezwungen, die Referenz in der Initialisiererliste jedes Konstruktors zu initialisieren: Es gibt keine Möglichkeit, diese Initialisierung in eine andere Funktion zu zerlegen ( bis C ++ 0x, jedenfalls bearbeiten: C ++ hat jetzt delegierende Konstruktoren ).
  • Die Referenz kann nicht zurückgebunden oder null sein. Dies kann von Vorteil sein, aber wenn der Code jemals geändert werden muss, um ein erneutes Binden zu ermöglichen, oder wenn das Mitglied null ist, müssen alle Verwendungen des Mitglieds geändert werden
  • Im Gegensatz zu Zeigerelementen können Referenzen nicht einfach durch intelligente Zeiger oder Iteratoren ersetzt werden, wie dies für das Refactoring erforderlich sein könnte
  • Wenn eine Referenz verwendet wird, sieht sie wie ein Werttyp ( .Operator usw.) aus, verhält sich jedoch wie ein Zeiger (kann baumeln) - z. B. rät Google Style Guide davon ab
James Hopkin
quelle
65
Alle Dinge, die Sie erwähnt haben, sind gute Dinge, die Sie vermeiden sollten. Wenn Referenzen dabei helfen, sind sie gut und nicht schlecht. Die Initialisierungsliste ist der beste Ort, um die Daten zu initialisieren. Sehr oft müssen Sie den Zuweisungsoperator ausblenden, mit Referenzen, die Sie nicht benötigen. "Kann nicht zurückgebunden werden" - gut, die Wiederverwendung von Variablen ist eine schlechte Idee.
Mykola Golubyev
4
@ Mykola: Ich stimme dir zu. Ich bevorzuge, dass Mitglieder in Initialisiererlisten initialisiert werden, ich vermeide Nullzeiger und es ist sicherlich nicht gut für eine Variable, ihre Bedeutung zu ändern. Unsere Meinungen unterscheiden sich darin, dass ich den Compiler nicht brauche oder möchte, um dies für mich durchzusetzen. Es macht das Schreiben von Klassen nicht einfacher, ich bin in diesem Bereich nicht auf Fehler gestoßen, die es abfangen würde, und gelegentlich schätze ich die Flexibilität, die ich durch Code erhalte, der konsequent Zeigerelemente verwendet (gegebenenfalls intelligente Zeiger).
James Hopkin
Ich denke, die Zuweisung nicht ausblenden zu müssen, ist ein falsches Argument, da Sie die Zuweisung nicht ausblenden müssen, wenn die Klasse einen Zeiger enthält, für den das Löschen ohnehin nicht verantwortlich ist - die Standardeinstellung reicht aus. Ich denke, dies ist die beste Antwort, da es gute Gründe gibt, warum Zeiger Referenzen in Mitgliedsdaten vorgezogen werden sollten.
Mark44 44
4
James, es ist nicht so, dass es das Schreiben des Codes erleichtert, sondern dass es das Lesen des Codes erleichtert. Mit einer Referenz als Datenelement müssen Sie sich nie fragen, ob sie am Verwendungsort null sein kann. Dies bedeutet, dass Sie Code mit weniger Kontext anzeigen können.
Len Holgate
4
Dies macht das Testen und Verspotten von Einheiten viel schwieriger, fast unmöglich, fast abhängig davon, wie viele verwendet werden. In Kombination mit der „Explosion von Affektgraphen“ sind meiner Meinung nach zwingende Gründe, sie niemals zu verwenden.
Chris Huang-Leaver
155

Meine eigene Faustregel:

  • Verwenden Sie ein Referenzelement, wenn Sie möchten, dass die Lebensdauer Ihres Objekts von der Lebensdauer anderer Objekte abhängt : Es ist eine explizite Möglichkeit zu sagen, dass Sie nicht zulassen, dass das Objekt ohne eine gültige Instanz einer anderen Klasse lebt - wegen no Zuordnung und die Verpflichtung, die Referenzinitialisierung über den Konstruktor zu erhalten. Dies ist eine gute Möglichkeit, Ihre Klasse zu entwerfen, ohne davon auszugehen, dass die Instanz Mitglied einer anderen Klasse ist oder nicht. Sie gehen nur davon aus, dass ihr Leben direkt mit anderen Instanzen verbunden ist. Sie können später ändern, wie Sie Ihre Klasseninstanz verwenden (mit new, als lokale Instanz, als Klassenmitglied, generiert durch einen Speicherpool in einem Manager usw.)
  • In anderen Fällen Zeiger verwenden : Wenn Sie möchten, dass das Element später geändert wird, verwenden Sie einen Zeiger oder einen konstanten Zeiger, um sicherzustellen, dass nur die spitze Instanz gelesen wird. Wenn dieser Typ kopierbar sein soll, können Sie ohnehin keine Referenzen verwenden. Manchmal müssen Sie das Mitglied auch nach einem speziellen Funktionsaufruf (z. B. init ()) initialisieren, und dann haben Sie einfach keine andere Wahl, als einen Zeiger zu verwenden. ABER: Verwenden Sie Asserts in all Ihren Member-Funktionen, um schnell einen falschen Zeigerstatus zu erkennen!
  • In Fällen, in denen die Objektlebensdauer von der Lebensdauer eines externen Objekts abhängen soll und dieser Typ auch kopierbar sein muss, verwenden Sie Zeigerelemente, aber Referenzargument im Konstruktor. Auf diese Weise geben Sie bei der Konstruktion an, dass die Lebensdauer dieses Objekts abhängt während der Lebensdauer des Arguments, ABER die Implementierung verwendet Zeiger, um weiterhin kopierbar zu sein. Solange diese Mitglieder nur durch Kopieren geändert werden und Ihr Typ keinen Standardkonstruktor hat, sollte der Typ beide Ziele erfüllen.
Klaim
quelle
1
Ich denke, Ihre und Mykolas sind richtige Antworten und fördern die zeigerlose Programmierung (weniger Zeiger lesen).
Amit
37

Objekte sollten selten Zuweisungen und andere Dinge wie Vergleiche zulassen. Wenn Sie ein Geschäftsmodell mit Objekten wie "Abteilung", "Mitarbeiter", "Direktor" betrachten, ist es schwer vorstellbar, dass ein Mitarbeiter einem anderen zugewiesen wird.

Für Geschäftsobjekte ist es daher sehr gut, Eins-zu-Eins- und Eins-zu-Viele-Beziehungen als Referenzen und nicht als Zeiger zu beschreiben.

Und wahrscheinlich ist es in Ordnung, eine Eins-oder-Null-Beziehung als Zeiger zu beschreiben.

Also kein 'wir können nicht zuweisen' dann Faktor.
Viele Programmierer gewöhnen sich nur an Zeiger, und deshalb finden sie Argumente, um die Verwendung von Referenzen zu vermeiden.

Wenn Sie einen Zeiger als Mitglied haben, werden Sie oder ein Mitglied Ihres Teams gezwungen, den Zeiger vor der Verwendung immer wieder zu überprüfen, mit dem Kommentar "Nur für den Fall". Wenn ein Zeiger Null sein kann, wird der Zeiger wahrscheinlich als eine Art Flag verwendet, was schlecht ist, da jedes Objekt seine eigene Rolle spielen muss.

Mykola Golubyev
quelle
2
+1: Das gleiche habe ich erlebt. Nur einige generische Datenspeicherklassen benötigen Zuweisung und Kopierfunktion. Und für übergeordnete Geschäftsobjekt-Frameworks sollte es andere Kopiertechniken geben, die auch Dinge wie "Keine eindeutigen Felder kopieren" und "Zu einem anderen übergeordneten Element hinzufügen" usw. verarbeiten. Sie verwenden also das übergeordnete Framework zum Kopieren Ihrer Geschäftsobjekte und verbieten Sie die Zuweisungen auf niedriger Ebene.
mmmmmmmm
2
+1: Einige Übereinstimmungen: Referenzmitglieder, die verhindern, dass ein Zuweisungsoperator definiert wird, sind kein wichtiges Argument. Wie Sie richtig anders angeben, haben nicht alle Objekte eine Wertsemantik. Ich stimme auch zu, dass wir nicht wollen, dass 'if (p)' ohne logischen Grund über den gesamten Code verteilt ist. Der richtige Ansatz hierfür ist jedoch die Klasseninvariante: Eine genau definierte Klasse lässt keinen Zweifel daran, ob Mitglieder null sein können. Wenn ein Zeiger im Code null sein kann, würde ich erwarten, dass er gut kommentiert wird.
James Hopkin
@ JamesHopkin Ich stimme zu, dass gut gestaltete Klassen Zeiger effektiv verwenden können. Referenzen schützen nicht nur vor NULL, sondern garantieren auch, dass Ihre Referenz initialisiert wurde (ein Zeiger muss nicht initialisiert werden, damit er auf etwas anderes verweisen kann). Referenzen auch sagen Sie Eigentum - nämlich , dass das Objekt nicht das Mitglied nicht besitzt. Wenn Sie konsistent Referenzen für aggregierte Elemente verwenden, haben Sie ein sehr hohes Maß an Sicherheit, dass Zeigerelemente zusammengesetzte Objekte sind (obwohl es nicht viele Fälle gibt, in denen ein zusammengesetztes Element durch einen Zeiger dargestellt wird).
weberc2
11

Verwenden Sie Referenzen, wenn Sie können, und Zeiger, wenn Sie müssen.

ebo
quelle
7
Ich hatte das gelesen, aber ich dachte, es ging um die Übergabe von Parametern, nicht um Mitgliedsdaten. "Referenzen erscheinen normalerweise auf der Haut eines Objekts und Zeiger auf der Innenseite."
Markh44
Ja, ich denke nicht, dass dies ein guter Rat im Zusammenhang mit Mitgliedsreferenzen ist.
Tim Angus
6

In einigen wichtigen Fällen ist eine Zuweisbarkeit einfach nicht erforderlich. Hierbei handelt es sich häufig um einfache Algorithmus-Wrapper, die die Berechnung erleichtern, ohne den Gültigkeitsbereich zu verlassen. Solche Objekte sind Hauptkandidaten für Referenzmitglieder, da Sie sicher sein können, dass sie immer eine gültige Referenz enthalten und niemals kopiert werden müssen.

Stellen Sie in solchen Fällen sicher, dass der Zuweisungsoperator (und häufig auch der Kopierkonstruktor) nicht verwendbar ist (indem Sie ihn erben boost::noncopyableoder als privat deklarieren).

Wie bereits von Benutzern kommentiert, gilt dies jedoch nicht für die meisten anderen Objekte. Hier kann die Verwendung von Referenzelementen ein großes Problem sein und sollte generell vermieden werden.

Konrad Rudolph
quelle
6

Da jeder allgemeine Regeln zu verteilen scheint, biete ich zwei an:

  • Verwenden Sie niemals Verweise als Klassenmitglieder. Ich habe dies in meinem eigenen Code noch nie getan (außer um mir selbst zu beweisen, dass ich mit dieser Regel Recht hatte) und kann mir keinen Fall vorstellen, in dem ich dies tun würde. Die Semantik ist zu verwirrend und es ist wirklich nicht das, wofür Referenzen entworfen wurden.

  • Verwenden Sie immer, immer Referenzen, wenn Sie Parameter an Funktionen übergeben, mit Ausnahme der Basistypen, oder wenn der Algorithmus eine Kopie erfordert.

Diese Regeln sind einfach und haben mir gute Dienste geleistet. Ich überlasse es anderen, Regeln für die Verwendung intelligenter Zeiger (aber bitte nicht auto_ptr) als Klassenmitglieder festzulegen.


quelle
2
Stimmen Sie dem ersten nicht ganz zu, aber Meinungsverschiedenheiten sind kein Thema, um die Begründung zu bewerten. +1
David Rodríguez - Dribeas
(Wir scheinen dieses Argument mit den guten Wählern von SO zu verlieren)
James Hopkin
2
Ich habe abgelehnt, weil "Niemals Referenzen als Klassenmitglieder verwenden" und die folgende Begründung für mich nicht richtig ist. Wie in meiner Antwort (und im Kommentar zur Top-Antwort) und aus eigener Erfahrung erläutert, gibt es Fälle, in denen es einfach eine gute Sache ist. Ich dachte wie Sie, bis ich in einem Unternehmen eingestellt wurde, in dem sie es gut nutzten, und es machte mir klar, dass es Fälle gibt, in denen es hilft. Tatsächlich denke ich, dass das eigentliche Problem eher in der Syntax und den versteckten Implikationen liegt, die die Verwendung von Referenzen als Mitglieder erfordern, damit der Programmierer versteht, was er tut.
Klaim
1
Neil> Ich stimme zu, aber es ist nicht die "Meinung", die mich zum Abstimmen gebracht hat, sondern die "nie" Behauptung. Da das Streiten hier sowieso nicht hilfreich zu sein scheint, werde ich meine Abwahl absagen.
Klaim
2
Diese Antwort könnte verbessert werden, indem erklärt wird, was mit der Semantik von Referenzmitgliedern verwirrend ist. Diese Begründung ist wichtig, da Zeiger auf den ersten Blick semantisch mehrdeutiger erscheinen (sie werden möglicherweise nicht initialisiert und geben keinen Aufschluss über den Besitz des zugrunde liegenden Objekts).
weberc2
4

Ja zu: Stimmt es, dass ein Objekt, das eine Referenz enthält, nicht zuweisbar sein sollte, da eine Referenz nach der Initialisierung nicht geändert werden sollte?

Meine Faustregeln für Datenmitglieder:

  • Verwenden Sie niemals eine Referenz, da dies die Zuweisung verhindert
  • Wenn Ihre Klasse für das Löschen verantwortlich ist, verwenden Sie das scoped_ptr von boost (das sicherer ist als ein auto_ptr).
  • Andernfalls verwenden Sie einen Zeiger oder einen konstanten Zeiger
pts
quelle
2
Ich kann nicht einmal einen Fall nennen, in dem ich eine Zuordnung des Geschäftsobjekts benötigen würde.
Mykola Golubyev
1
+1: Ich möchte nur hinzufügen, um vorsichtig mit auto_ptr-Mitgliedern zu sein - jede Klasse, in der sie vorhanden sind, muss eine explizit definierte Zuweisung und Kopierkonstruktion haben (es ist sehr unwahrscheinlich, dass die generierten Elemente erforderlich sind)
James Hopkin
auto_ptr verhindert auch eine (sinnvolle) Zuordnung.
2
@Mykola: Ich habe auch die Erfahrung gemacht, dass 98% meiner Geschäftsobjekte keine Zuordnung oder Kopierer benötigen. Ich verhindere dies durch ein kleines Makro, das ich zu den Headern dieser Klassen hinzufüge, wodurch copy c'tors und operator = () ohne Implementierung privat werden. Also in den 98% der Objekte verwende ich auch Referenzen und es ist sicher.
mmmmmmmm
1
Verweise als Mitglieder können verwendet werden, um explizit anzugeben, dass die Lebensdauer der Instanz der Klasse direkt von der Lebensdauer der Instanzen anderer Klassen (oder derselben) abhängt. "Verwenden Sie niemals eine Referenz, da dies die Zuweisung verhindert" setzt voraus, dass alle Klassen die Zuweisung zulassen müssen. Es ist wie angenommen, dass alle Klassen Kopiervorgänge zulassen müssen ...
Klaim
2

Ich würde im Allgemeinen nur dann einen Zeiger in Mitgliedsdaten verwenden, wenn der Zeiger null sein oder sich ändern könnte. Gibt es andere Gründe, Zeiger gegenüber Referenzen für Datenelemente vorzuziehen?

Ja. Lesbarkeit Ihres Codes. Ein Zeiger macht deutlicher, dass das Element eine Referenz ist (ironischerweise :)) und kein enthaltenes Objekt, denn wenn Sie es verwenden, müssen Sie es de-referenzieren. Ich weiß, dass einige Leute denken, dass das altmodisch ist, aber ich denke immer noch, dass es einfach Verwirrung und Fehler verhindert.

Dani van der Meer
quelle
1
Lakos argumentiert dies auch in "Large-Scale C ++ Software Design".
Johan Kotlinski
Ich glaube, Code Complete befürwortet dies auch und bin nicht dagegen. Es ist jedoch nicht übermäßig überzeugend ...
markh44
2

Ich rate von Referenzdatenmitgliedern ab, da Sie nie wissen, wer von Ihrer Klasse abgeleitet wird und was sie möglicherweise tun möchten. Möglicherweise möchten sie das referenzierte Objekt nicht verwenden, aber als Referenz haben Sie sie gezwungen, ein gültiges Objekt anzugeben. Ich habe mir das genug angetan, um keine Referenzdatenelemente mehr zu verwenden.

StephenD
quelle
0

Wenn ich die Frage richtig verstehe ...

Referenz als Funktionsparameter anstelle des Zeigers: Wie Sie bereits betont haben, macht ein Zeiger nicht klar, wem die Bereinigung / Initialisierung des Zeigers gehört. Bevorzugen Sie einen gemeinsamen Punkt, wenn Sie einen Zeiger möchten, der Teil von C ++ 11 ist, und einen schwachen_ptr, wenn die Gültigkeit der Daten nicht über die Lebensdauer der Klasse garantiert ist, die die Daten akzeptiert, auf die verwiesen wird.; auch ein Teil von C ++ 11. Die Verwendung einer Referenz als Funktionsparameter garantiert, dass die Referenz nicht null ist. Sie müssen die Sprachfunktionen untergraben, um dies zu umgehen, und wir kümmern uns nicht um lose Kanonencodierer.

Referenz aa Mitgliedsvariable: Wie oben in Bezug auf die Datengültigkeit. Dies garantiert, dass die Daten, auf die verwiesen wird und auf die dann verwiesen wird, gültig sind.

Wenn Sie die Verantwortung für die Gültigkeit von Variablen auf einen früheren Punkt im Code verschieben, wird nicht nur der spätere Code (in Ihrem Beispiel die Klasse A) bereinigt, sondern auch der verwendeten Person klar gemacht.

In Ihrem Beispiel, das etwas verwirrend ist (ich würde wirklich versuchen, eine klarere Implementierung zu finden), ist das von B verwendete A für die Lebensdauer des B garantiert, da B ein Mitglied von A ist, sodass eine Referenz dies verstärkt und ist (vielleicht) klarer.

Und nur für den Fall (was eine geringe Wahrscheinlichkeit ist, da dies in Ihrem Codekontext keinen Sinn ergibt), würde eine andere Alternative, einen nicht referenzierten Parameter ohne Zeiger A zu verwenden, A kopieren und das Paradigma unbrauchbar machen - I. Ich glaube wirklich nicht, dass du das als Alternative meinst.

Dies garantiert auch, dass Sie die referenzierten Daten nicht ändern können, wenn ein Zeiger geändert werden kann. Ein const-Zeiger würde nur funktionieren, wenn der referenzierte / Zeiger auf Daten nicht veränderbar wäre.

Zeiger sind nützlich, wenn der A-Parameter für B nicht garantiert eingestellt wurde oder neu zugewiesen werden konnte. Und manchmal ist ein schwacher Zeiger einfach zu ausführlich für die Implementierung, und eine gute Anzahl von Leuten weiß entweder nicht, was ein schwacher_ptr ist, oder sie mögen sie einfach nicht.

Zerreiße diese Antwort bitte :)

Kit10
quelle